diff --git a/.vscode/settings.json b/.vscode/settings.json index f3e0823..c831fde 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { // IntelliSense for taiwind variants "tailwindCSS.experimental.classRegex": [ - ["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"] + "tv\\('([^)]*)\\')", + "(?:'|\"|`)([^\"'`]*)(?:'|\"|`)" ] } diff --git a/packages/backend/src/registry.ts b/packages/backend/src/registry.ts index 8a6e489..ed30af9 100644 --- a/packages/backend/src/registry.ts +++ b/packages/backend/src/registry.ts @@ -12,12 +12,14 @@ import { ApplicationDeploymentRequest } from './entity/Deployment'; import { AppDeploymentRecord, PackageJSON } from './types'; +import { sleep } from './utils'; const log = debug('snowball:registry'); const APP_RECORD_TYPE = 'ApplicationRecord'; const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest'; const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord'; +const SLEEP_DURATION = 1000; // TODO: Move registry code to laconic-sdk/watcher-ts export class Registry { @@ -111,16 +113,21 @@ export class Registry { const crn = this.getCrn(appName); log(`Setting name: ${crn} for record ID: ${result.data.id}`); + await sleep(SLEEP_DURATION); await this.registry.setName( { cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee ); + + await sleep(SLEEP_DURATION); await this.registry.setName( { cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee ); + + await sleep(SLEEP_DURATION); await this.registry.setName( { cid: result.data.id, @@ -139,9 +146,9 @@ export class Registry { async createApplicationDeploymentRequest (data: { deployment: Deployment, appName: string, - packageJsonName: string, repository: string, - environmentVariables: { [key: string]: string } + environmentVariables: { [key: string]: string }, + dns: string, }): Promise<{ applicationDeploymentRequestId: string; applicationDeploymentRequestData: ApplicationDeploymentRequest; @@ -160,7 +167,7 @@ export class Registry { version: '1.0.0', name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`, application: `${crn}@${applicationRecord.attributes.app_version}`, - dns: `${data.deployment.project.name}-${data.deployment.id}`, + dns: data.dns, // TODO: Not set in test-progressive-web-app CI // deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN', @@ -178,6 +185,7 @@ export class Registry { }) }; + await sleep(SLEEP_DURATION); const result = await this.registry.setRecord( { privateKey: this.registryConfig.privateKey, @@ -211,11 +219,12 @@ export class Registry { true ); - // Filter records with ApplicationRecord ids + // Filter records with ApplicationRecord ID and Deployment specific URL return records.filter((record: AppDeploymentRecord) => deployments.some( (deployment) => - deployment.applicationRecordId === record.attributes.application + deployment.applicationRecordId === record.attributes.application && + record.attributes.url.includes(deployment.id) ) ); } diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index d204537..5211585 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -382,8 +382,7 @@ export class Service { async createDeployment ( userId: string, octokit: Octokit, - data: DeepPartial, - recordData: { repoUrl?: string } = {} + data: DeepPartial ): Promise { assert(data.project?.repository, 'Project repository not found'); log( @@ -407,13 +406,10 @@ export class Service { assert(packageJSON.name, "name field doesn't exist in package.json"); - if (!recordData.repoUrl) { - const { data: repoDetails } = await octokit.rest.repos.get({ - owner, - repo - }); - recordData.repoUrl = repoDetails.html_url; - } + const repoUrl = (await octokit.rest.repos.get({ + owner, + repo + })).data.html_url; // TODO: Set environment variables for each deployment (environment variables can`t be set in application record) const { applicationRecordId, applicationRecordData } = @@ -422,7 +418,7 @@ export class Service { packageJSON, appType: data.project!.template!, commitHash: data.commitHash!, - repoUrl: recordData.repoUrl + repoUrl }); // Update previous deployment with prod branch domain @@ -464,11 +460,23 @@ export class Service { { deployment: newDeployment, appName: repo, - packageJsonName: packageJSON.name, - repository: recordData.repoUrl, - environmentVariables: environmentVariablesObj + repository: repoUrl, + environmentVariables: environmentVariablesObj, + dns: `${newDeployment.project.name}-${newDeployment.id}` }); + // To set project DNS + if (data.environment === Environment.Production) { + await this.registry.createApplicationDeploymentRequest( + { + deployment: newDeployment, + appName: repo, + repository: repoUrl, + environmentVariables: environmentVariablesObj, + dns: `${newDeployment.project.name}` + }); + } + await this.db.updateDeploymentById(newDeployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData }); return newDeployment; @@ -498,8 +506,6 @@ export class Service { per_page: 1 }); - const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo }); - // Create deployment with prod branch and latest commit await this.createDeployment(user.id, octokit, @@ -510,9 +516,6 @@ export class Service { domain: null, commitHash: latestCommit.sha, commitMessage: latestCommit.commit.message - }, - { - repoUrl: repoDetails.html_url } ); @@ -555,8 +558,14 @@ export class Service { } async handleGitHubPush (data: GitPushEventPayload): Promise { - const { repository, ref, head_commit: headCommit } = data; - log(`Handling GitHub push event from repository: ${repository.full_name}`); + const { repository, ref, head_commit: headCommit, deleted } = data; + + if (deleted) { + log(`Branch ${ref} deleted for project ${repository.full_name}`); + return; + } + + log(`Handling GitHub push event from repository: ${repository.full_name}, branch: ${ref}`); const projects = await this.db.getProjects({ where: { repository: repository.full_name } }); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index eae6929..8094134 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -24,6 +24,7 @@ export interface GitPushEventPayload { id: string; message: string; }; + deleted: boolean; } export interface AppDeploymentRecordAttributes { diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts index 693223b..8d2a8e8 100644 --- a/packages/backend/src/utils.ts +++ b/packages/backend/src/utils.ts @@ -66,3 +66,5 @@ export const loadAndSaveData = async ( return savedEntity; }; + +export const sleep = async (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/packages/deployer/README.md b/packages/deployer/README.md index 04c5d6b..d297685 100644 --- a/packages/deployer/README.md +++ b/packages/deployer/README.md @@ -8,57 +8,6 @@ brew install jq # if you do not have jq installed already ``` - - - -Example of how to make the necessary deploy edits [here](https://github.com/snowball-tools/snowballtools-base/pull/131/files). - -- Replace variables in the following files - - [records/application-deployment-request.yml](records/application-deployment-request.yml) - - update the name & application version numbers - - ``: Replace with current time which can be generated by command `date -u` - ```yml - # Example - record: - ... - meta: - note: Added by Snowball @ Friday 23 February 2024 06:35:50 AM UTC - ... - ``` - -- Update record version in [records/application-record.yml](records/application-record.yml) - ```yml - record: - type: ApplicationRecord - version: - ... - ``` - -- Update commit hash in the following places: - - [records/application-record.yml](records/application-record.yml) - ```yml - record: - ... - repository_ref: - ... - ``` - - [records/application-deployment-request.yml](records/application-deployment-request.yml) - ```yml - record: - ... - meta: - ... - repository_ref: - ``` - - [deploy-frontend.sh](deploy-frontend.sh) - Also be sure to update the app version - ```bash - ... - RCD_APP_VERSION="" - REPO_REF="" - ... - ``` - - Run script to deploy app ``` ./deploy-frontend.sh diff --git a/packages/deployer/config.yml b/packages/deployer/config.yml index f65a3bf..4eeaf9a 100644 --- a/packages/deployer/config.yml +++ b/packages/deployer/config.yml @@ -2,8 +2,8 @@ services: cns: restEndpoint: http://console.laconic.com:1317 gqlEndpoint: http://console.laconic.com:9473/api + userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311 + bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 chainId: laconic_9000-1 gas: 1200000 fees: 200000aphoton - userKey: 0524fc22ea0a12e6c5cc4cfe08e73c95dffd0ab5ed72a59f459ed33134fa3b16 - bondId: 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac diff --git a/packages/deployer/deploy-frontend.sh b/packages/deployer/deploy-frontend.sh index 3bff669..9251042 100755 --- a/packages/deployer/deploy-frontend.sh +++ b/packages/deployer/deploy-frontend.sh @@ -12,18 +12,17 @@ PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json) # Current date and time for note CURRENT_DATE_TIME=$(date -u) -# Increment application-record version -APPLICATION_RECORD_FILE="./records/application-record.yml" -if [ -f "$APPLICATION_RECORD_FILE" ]; then - # Extract current version and increment it - CURRENT_VERSION=$(grep 'version:' $APPLICATION_RECORD_FILE | head -1 | awk '{print $2}') - IFS='.' read -ra ADDR <<< "$CURRENT_VERSION" - VERSION_NUMBER=${ADDR[2]} - NEW_VERSION_NUMBER=$((VERSION_NUMBER + 1)) - NEW_APPLICATION_VERSION="${ADDR[0]}.${ADDR[1]}.$NEW_VERSION_NUMBER" -else - # If file does not exist, start from version 0.0.1 - NEW_APPLICATION_VERSION="0.0.1" +CONFIG_FILE=config.yml +REGISTRY_BOND_ID="8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac" + +# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts + +# Get latest version from registry and increment application-record version +NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}') + +if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then + # Set application-record version if no previous records were found + NEW_APPLICATION_VERSION=0.0.1 fi # Generate application-deployment-request.yml @@ -62,10 +61,7 @@ EOF echo "Files generated successfully." -# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts - RECORD_FILE=records/application-record.yml -CONFIG_FILE=config.yml # Publish ApplicationRecord RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id') @@ -75,8 +71,11 @@ echo $RECORD_ID # Set name to record REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend" +sleep 2 yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID" +sleep 2 yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID" +sleep 2 # Set name if latest release yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID" echo "$REGISTRY_APP_CRN set for ApplicationRecord" @@ -90,6 +89,7 @@ fi RECORD_FILE=records/application-deployment-request.yml +sleep 2 DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id') echo "ApplicationDeploymentRequest published" echo $DEPLOYMENT_REQUEST_ID diff --git a/packages/deployer/records/application-deployment-request.yml b/packages/deployer/records/application-deployment-request.yml index a543e47..8551abf 100644 --- a/packages/deployer/records/application-deployment-request.yml +++ b/packages/deployer/records/application-deployment-request.yml @@ -13,6 +13,6 @@ record: LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2 meta: - note: Added by Snowball @ Thu Feb 29 02:48:23 UTC 2024 + note: Added by Snowball @ Thursday 29 February 2024 04:36:04 PM UTC repository: "https://git.vdb.to/cerc-io/snowballtools-base" - repository_ref: 94a9bf88a9cf8442ffd37ad924cf9a590af69431 + repository_ref: 1ff5ab3dfdba9dcf5dd1cb0f9435bd863a6d0340 diff --git a/packages/deployer/records/application-record.yml b/packages/deployer/records/application-record.yml index c5b718f..eceed26 100644 --- a/packages/deployer/records/application-record.yml +++ b/packages/deployer/records/application-record.yml @@ -1,7 +1,7 @@ record: type: ApplicationRecord - version: 0.0.25 - repository_ref: 94a9bf88a9cf8442ffd37ad924cf9a590af69431 + version: 0.0.1 + repository_ref: 1ff5ab3dfdba9dcf5dd1cb0f9435bd863a6d0340 repository: ["https://git.vdb.to/cerc-io/snowballtools-base"] app_type: webapp name: snowballtools-base-frontend diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 204b9f3..614068f 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -8,6 +8,7 @@ "@material-tailwind/react": "^2.1.7", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-switch": "^1.0.3", @@ -48,7 +49,7 @@ "siwe": "^2.1.4", "tailwind-variants": "^0.2.0", "typescript": "^4.9.5", - "usehooks-ts": "^2.10.0", + "usehooks-ts": "^2.15.1", "vertical-stepper-nav": "^1.0.2", "viem": "^2.7.11", "wagmi": "^2.5.7", diff --git a/packages/frontend/src/components/FormatMilliSecond.tsx b/packages/frontend/src/components/FormatMilliSecond.tsx index 047a547..bc8dcda 100644 --- a/packages/frontend/src/components/FormatMilliSecond.tsx +++ b/packages/frontend/src/components/FormatMilliSecond.tsx @@ -1,13 +1,22 @@ import { Duration } from 'luxon'; -import React from 'react'; +import React, { ComponentPropsWithoutRef } from 'react'; +import { cn } from 'utils/classnames'; -const FormatMillisecond = ({ time }: { time: number }) => { +export interface FormatMilliSecondProps + extends ComponentPropsWithoutRef<'div'> { + time: number; +} + +const FormatMillisecond = ({ time, ...props }: FormatMilliSecondProps) => { const formatTime = Duration.fromMillis(time) .shiftTo('days', 'hours', 'minutes', 'seconds') .toObject(); return ( -
+
{formatTime.days !== 0 && {formatTime.days}d } {formatTime.hours !== 0 && {formatTime.hours}h } {formatTime.minutes !== 0 && {formatTime.minutes}m } diff --git a/packages/frontend/src/components/StopWatch.tsx b/packages/frontend/src/components/StopWatch.tsx index 4e70f9b..239b119 100644 --- a/packages/frontend/src/components/StopWatch.tsx +++ b/packages/frontend/src/components/StopWatch.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useStopwatch } from 'react-timer-hook'; -import FormatMillisecond from './FormatMilliSecond'; +import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond'; const setStopWatchOffset = (time: string) => { const providedTime = new Date(time); @@ -11,13 +11,17 @@ const setStopWatchOffset = (time: string) => { return currentTime; }; -const Stopwatch = ({ offsetTimestamp }: { offsetTimestamp: Date }) => { +interface StopwatchProps extends Omit { + offsetTimestamp: Date; +} + +const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => { const { totalSeconds } = useStopwatch({ autoStart: true, offsetTimestamp: offsetTimestamp, }); - return ; + return ; }; export { Stopwatch, setStopWatchOffset }; diff --git a/packages/frontend/src/components/projects/create/Deploy.tsx b/packages/frontend/src/components/projects/create/Deploy.tsx index 6769895..a251773 100644 --- a/packages/frontend/src/components/projects/create/Deploy.tsx +++ b/packages/frontend/src/components/projects/create/Deploy.tsx @@ -1,11 +1,14 @@ import React, { useCallback, useEffect } from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; -import { Button, Typography } from '@material-tailwind/react'; +import { Typography } from '@material-tailwind/react'; import { DeployStep, DeployStatus } from './DeployStep'; import { Stopwatch, setStopWatchOffset } from '../../StopWatch'; import ConfirmDialog from 'components/shared/ConfirmDialog'; +import { Heading } from 'components/shared/Heading'; +import { Button } from 'components/shared/Button'; +import { ClockOutlineIcon, WarningIcon } from 'components/shared/CustomIcon'; const TIMEOUT_DURATION = 5000; const Deploy = () => { @@ -31,27 +34,27 @@ const Deploy = () => { }, []); return ( -
-
-
-

Deployment started ...

-
- ^  +
+
+
+ + Deployment started ... + +
+
-
- -
+ {
- - - - + +
+ + + + +
); }; diff --git a/packages/frontend/src/components/projects/create/DeployStep.tsx b/packages/frontend/src/components/projects/create/DeployStep.tsx index 1aae9c0..de78f78 100644 --- a/packages/frontend/src/components/projects/create/DeployStep.tsx +++ b/packages/frontend/src/components/projects/create/DeployStep.tsx @@ -1,11 +1,22 @@ import React, { useState } from 'react'; -import toast from 'react-hot-toast'; -import { Collapse, Button, Typography } from '@material-tailwind/react'; +import { Collapse } from '@material-tailwind/react'; import { Stopwatch, setStopWatchOffset } from '../../StopWatch'; import FormatMillisecond from '../../FormatMilliSecond'; import processLogs from '../../../assets/process-logs.json'; +import { cn } from 'utils/classnames'; +import { + CheckRoundFilledIcon, + ClockOutlineIcon, + CopyIcon, + LoaderIcon, + MinusCircleIcon, + PlusIcon, +} from 'components/shared/CustomIcon'; +import { Button } from 'components/shared/Button'; +import { useToast } from 'components/shared/Toast'; +import { useIntersectionObserver } from 'usehooks-ts'; enum DeployStatus { PROCESSING = 'progress', @@ -28,61 +39,115 @@ const DeployStep = ({ startTime, processTime, }: DeployStepsProps) => { - const [collapse, setCollapse] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const { toast, dismiss } = useToast(); + const { isIntersecting: hideGradientOverlay, ref } = useIntersectionObserver({ + threshold: 1, + }); + + const disableCollapse = status !== DeployStatus.COMPLETE; return ( -
-
- {status === DeployStatus.NOT_STARTED &&
{step}
} - {status === DeployStatus.PROCESSING &&
O
} - {status === DeployStatus.COMPLETE && ( -
- +
+ {/* Collapisble trigger */} +
- -
+ + + {/* Collapsible */} + +
+ {/* Logs */} {processLogs.map((log, key) => { return ( - +

{log} - +

); })} -
+ + {/* End of logs ref used for hiding gradient overlay */} +
+ + {/* Overflow gradient overlay */} + {!hideGradientOverlay && ( +
+ )} + + {/* Copy log button */} +
diff --git a/packages/frontend/src/components/projects/create/ProjectRepoCard.tsx b/packages/frontend/src/components/projects/create/ProjectRepoCard.tsx deleted file mode 100644 index fbcf50b..0000000 --- a/packages/frontend/src/components/projects/create/ProjectRepoCard.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React, { useCallback } from 'react'; -import toast from 'react-hot-toast'; -import { useNavigate, useParams } from 'react-router-dom'; - -import { Chip, IconButton, Spinner } from '@material-tailwind/react'; - -import { relativeTimeISO } from '../../../utils/time'; -import { GitRepositoryDetails } from '../../../types'; -import { useGQLClient } from '../../../context/GQLClientContext'; -import { GithubIcon, LockIcon } from 'components/shared/CustomIcon'; - -interface ProjectRepoCardProps { - repository: GitRepositoryDetails; -} - -const ProjectRepoCard: React.FC = ({ repository }) => { - const client = useGQLClient(); - const navigate = useNavigate(); - const [isLoading, setIsLoading] = React.useState(false); - - const { orgSlug } = useParams(); - - const createProject = useCallback(async () => { - if (!repository) { - return; - } - - setIsLoading(true); - const { addProject } = await client.addProject(orgSlug!, { - name: `${repository.owner!.login}-${repository.name}`, - prodBranch: repository.default_branch!, - repository: repository.full_name, - // TODO: Compute template from repo - template: 'webapp', - }); - - if (Boolean(addProject)) { - setIsLoading(false); - navigate(`import?projectId=${addProject.id}`); - } else { - setIsLoading(false); - toast.error('Failed to create project'); - } - }, [client, repository]); - - return ( -
-
- -
-
-
- {repository.full_name} - {repository.visibility === 'private' && ( - } - /> - )} -
-

{repository.updated_at && relativeTimeISO(repository.updated_at)}

-
- {isLoading ? ( - - ) : ( -
- - {'>'} - -
- )} -
- ); -}; - -export default ProjectRepoCard; diff --git a/packages/frontend/src/components/projects/create/ProjectRepoCard/ProjectRepoCard.tsx b/packages/frontend/src/components/projects/create/ProjectRepoCard/ProjectRepoCard.tsx new file mode 100644 index 0000000..b06fc23 --- /dev/null +++ b/packages/frontend/src/components/projects/create/ProjectRepoCard/ProjectRepoCard.tsx @@ -0,0 +1,114 @@ +import React, { useCallback, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; + +import { Spinner } from '@material-tailwind/react'; + +import { relativeTimeISO } from 'utils/time'; +import { GitRepositoryDetails } from 'types'; +import { useGQLClient } from 'context/GQLClientContext'; +import { + ArrowRightCircleIcon, + GithubIcon, + LockIcon, +} from 'components/shared/CustomIcon'; +import { Button } from 'components/shared/Button'; +import { useToast } from 'components/shared/Toast'; + +interface ProjectRepoCardProps { + repository: GitRepositoryDetails; +} + +export const ProjectRepoCard: React.FC = ({ + repository, +}) => { + const client = useGQLClient(); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + + const { orgSlug } = useParams(); + const { toast, dismiss } = useToast(); + + const createProject = useCallback(async () => { + if (!repository || !orgSlug) { + return toast({ + id: 'missing-repository-or-org-slug', + title: 'Repository or organization slug is missing', + variant: 'error', + onDismiss: dismiss, + }); + } + + try { + setIsLoading(true); + const { addProject } = await client.addProject(orgSlug, { + name: `${repository.owner?.login}-${repository.name}`, + prodBranch: repository.default_branch as string, + repository: repository.full_name, + // TODO: Compute template from repo + template: 'webapp', + }); + if (addProject) { + navigate(`import?projectId=${addProject.id}`); + } else { + toast({ + id: 'failed-to-create-project', + title: 'Failed to create project', + variant: 'error', + onDismiss: dismiss, + }); + } + } catch (error) { + console.error(error); + toast({ + id: 'failed-to-create-project', + title: 'Failed to create project', + variant: 'error', + onDismiss: dismiss, + }); + } finally { + setIsLoading(false); + } + }, [client, repository, orgSlug, setIsLoading, navigate, toast]); + + return ( +
+ {/* Icon container */} +
+ +
+ {/* Content */} +
+
+

+ {repository.full_name} +

+

+ {repository.updated_at && relativeTimeISO(repository.updated_at)} +

+
+ {repository.visibility === 'private' && ( +
+ + Private +
+ )} +
+ {/* Right action */} + {isLoading ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/packages/frontend/src/components/projects/create/ProjectRepoCard/index.ts b/packages/frontend/src/components/projects/create/ProjectRepoCard/index.ts new file mode 100644 index 0000000..7847249 --- /dev/null +++ b/packages/frontend/src/components/projects/create/ProjectRepoCard/index.ts @@ -0,0 +1 @@ +export * from './ProjectRepoCard'; diff --git a/packages/frontend/src/components/projects/create/RepositoryList.tsx b/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx similarity index 58% rename from packages/frontend/src/components/projects/create/RepositoryList.tsx rename to packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx index f09e61d..14af57e 100644 --- a/packages/frontend/src/components/projects/create/RepositoryList.tsx +++ b/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx @@ -3,13 +3,17 @@ import { Octokit } from 'octokit'; import assert from 'assert'; import { useDebounce } from 'usehooks-ts'; -import { Button, Typography, Option } from '@material-tailwind/react'; +import { Button, Typography } from '@material-tailwind/react'; -import SearchBar from '../../SearchBar'; -import ProjectRepoCard from './ProjectRepoCard'; -import { GitOrgDetails, GitRepositoryDetails } from '../../../types'; -import AsyncSelect from '../../shared/AsyncSelect'; -import { GithubIcon } from 'components/shared/CustomIcon'; +import { ProjectRepoCard } from 'components/projects/create/ProjectRepoCard'; +import { GitOrgDetails, GitRepositoryDetails } from 'types'; +import { + ChevronGrabberHorizontal, + GithubIcon, + SearchIcon, +} from 'components/shared/CustomIcon'; +import { Select, SelectOption } from 'components/shared/Select'; +import { Input } from 'components/shared/Input'; const DEFAULT_SEARCHED_REPO = ''; const REPOS_PER_PAGE = 5; @@ -18,9 +22,9 @@ interface RepositoryListProps { octokit: Octokit; } -const RepositoryList = ({ octokit }: RepositoryListProps) => { +export const RepositoryList = ({ octokit }: RepositoryListProps) => { const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO); - const [selectedAccount, setSelectedAccount] = useState(''); + const [selectedAccount, setSelectedAccount] = useState(); const [orgs, setOrgs] = useState([]); // TODO: Add new type for Git user when required const [gitUser, setGitUser] = useState(); @@ -35,7 +39,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => { const orgs = await octokit.rest.orgs.listForAuthenticatedUser(); setOrgs(orgs.data); setGitUser(user.data); - setSelectedAccount(user.data.login); + setSelectedAccount({ label: user.data.login, value: user.data.login }); }; fetchUserAndOrgs(); @@ -54,7 +58,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => { let query = `${debouncedSearchedRepo} in:name fork:true`; // Check if selected account is an organization - if (selectedAccount === gitUser.login) { + if (selectedAccount.value === gitUser.login) { query = query + ` user:${selectedAccount}`; } else { query = query + ` org:${selectedAccount}`; @@ -69,7 +73,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => { return; } - if (selectedAccount === gitUser.login) { + if (selectedAccount.value === gitUser.login) { const result = await octokit.rest.repos.listForAuthenticatedUser({ per_page: REPOS_PER_PAGE, affiliation: 'owner', @@ -78,7 +82,9 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => { return; } - const selectedOrg = orgs.find((org) => org.login === selectedAccount); + const selectedOrg = orgs.find( + (org) => org.login === selectedAccount.value, + ); assert(selectedOrg, 'Selected org not found in list'); const result = await octokit.rest.repos.listForOrg({ @@ -96,7 +102,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => { const handleResetFilters = useCallback(() => { assert(gitUser, 'Git user is not available'); setSearchedRepo(DEFAULT_SEARCHED_REPO); - setSelectedAccount(gitUser.login); + setSelectedAccount({ label: gitUser.login, value: gitUser.login }); }, [gitUser]); const accounts = useMemo(() => { @@ -107,35 +113,52 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => { return [gitUser, ...orgs]; }, [octokit, orgs, gitUser]); + const options = useMemo(() => { + return accounts.map((account) => ({ + label: account.login, + value: account.login, + leftIcon: , + })); + }, [accounts]); + return ( -
-
-
- + {/* Dropdown and search */} +
+
+ setSearchedRepo(event.target.value)} placeholder="Search for repository" + leftIcon={} + onChange={(e) => setSearchedRepo(e.target.value)} />
+ + {/* Repository list */} {Boolean(repositoryDetails.length) ? ( - repositoryDetails.map((repo, key) => { - return ; - }) +
+ {repositoryDetails.map((repo, index) => ( + <> + + {/* Horizontal line */} + {index !== repositoryDetails.length - 1 && ( +
+ )} + + ))} +
) : (
@@ -151,8 +174,6 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
)} -
+ ); }; - -export default RepositoryList; diff --git a/packages/frontend/src/components/projects/create/RepositoryList/index.ts b/packages/frontend/src/components/projects/create/RepositoryList/index.ts new file mode 100644 index 0000000..dc3bc8c --- /dev/null +++ b/packages/frontend/src/components/projects/create/RepositoryList/index.ts @@ -0,0 +1 @@ +export * from './RepositoryList'; diff --git a/packages/frontend/src/components/projects/create/TemplateCard/TemplateCard.tsx b/packages/frontend/src/components/projects/create/TemplateCard/TemplateCard.tsx index ad073c2..afae482 100644 --- a/packages/frontend/src/components/projects/create/TemplateCard/TemplateCard.tsx +++ b/packages/frontend/src/components/projects/create/TemplateCard/TemplateCard.tsx @@ -55,9 +55,9 @@ export const TemplateCard: React.FC = ({ }, [orgSlug, dismiss, isGitAuth, navigate, template, toast]); return ( - )} - +
); }; diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx index e191fe8..c4940ba 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx @@ -1,31 +1,28 @@ -import React, { useState } from 'react'; -import toast from 'react-hot-toast'; +import React from 'react'; import { + Deployment, + DeploymentStatus, + Domain, Environment, Project, - Domain, - DeploymentStatus, - Deployment, } from 'gql-client'; - +import { Avatar } from 'components/shared/Avatar'; import { - Menu, - MenuHandler, - MenuList, - MenuItem, - Typography, - Chip, - ChipProps, - Tooltip, -} from '@material-tailwind/react'; - -import { relativeTimeMs } from '../../../../utils/time'; -import ConfirmDialog from '../../../shared/ConfirmDialog'; -import DeploymentDialogBodyCard from './DeploymentDialogBodyCard'; -import AssignDomainDialog from './AssignDomainDialog'; -import { useGQLClient } from '../../../../context/GQLClientContext'; + BranchStrokeIcon, + CheckRoundFilledIcon, + ClockOutlineIcon, + CommitIcon, + LoadingIcon, + WarningIcon, +} from 'components/shared/CustomIcon'; +import { Heading } from 'components/shared/Heading'; +import { OverflownText } from 'components/shared/OverflownText'; +import { Tag, TagTheme } from 'components/shared/Tag'; +import { getInitials } from 'utils/geInitials'; +import { relativeTimeMs } from 'utils/time'; import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants'; import { formatAddress } from '../../../../utils/format'; +import { DeploymentMenu } from './DeploymentMenu'; interface DeployDetailsCardProps { deployment: Deployment; @@ -35,10 +32,12 @@ interface DeployDetailsCardProps { prodBranchDomains: Domain[]; } -const STATUS_COLORS: { [key in DeploymentStatus]: ChipProps['color'] } = { - [DeploymentStatus.Building]: 'blue', - [DeploymentStatus.Ready]: 'green', - [DeploymentStatus.Error]: 'red', +const STATUS_COLORS: { + [key in DeploymentStatus]: TagTheme['type']; +} = { + [DeploymentStatus.Building]: 'emphasized', + [DeploymentStatus.Ready]: 'positive', + [DeploymentStatus.Error]: 'negative', }; const DeploymentDetailsCard = ({ @@ -48,241 +47,99 @@ const DeploymentDetailsCard = ({ project, prodBranchDomains, }: DeployDetailsCardProps) => { - const client = useGQLClient(); - - const [changeToProduction, setChangeToProduction] = useState(false); - const [redeployToProduction, setRedeployToProduction] = useState(false); - const [rollbackDeployment, setRollbackDeployment] = useState(false); - const [assignDomainDialog, setAssignDomainDialog] = useState(false); - - const updateDeployment = async () => { - const isUpdated = await client.updateDeploymentToProd(deployment.id); - if (isUpdated) { - await onUpdate(); - toast.success('Deployment changed to production'); - } else { - toast.error('Unable to change deployment to production'); + const getIconByDeploymentStatus = (status: DeploymentStatus) => { + if (status === DeploymentStatus.Building) { + return ; } - }; - - const redeployToProd = async () => { - const isRedeployed = await client.redeployToProd(deployment.id); - if (isRedeployed) { - await onUpdate(); - toast.success('Redeployed to production'); - } else { - toast.error('Unable to redeploy to production'); + if (status === DeploymentStatus.Ready) { + return ; } - }; - const rollbackDeploymentHandler = async () => { - const isRollbacked = await client.rollbackDeployment( - project.id, - deployment.id, - ); - if (isRollbacked) { - await onUpdate(); - toast.success('Deployment rolled back'); - } else { - toast.error('Unable to rollback deployment'); + if (status === DeploymentStatus.Error) { + return ; } }; return ( -
-
-
- {deployment.url && ( - +
+
+ {/* DEPLOYMENT URL */} + {deployment.url && ( + + {deployment.url} - - )} -
- + + + )} + {deployment.environment === Environment.Production ? `Production ${deployment.isCurrent ? '(Current)' : ''}` : 'Preview'} - +
-
- ^} + + {/* DEPLOYMENT STATUS */} +
+ + {deployment.status} + +
+ + {/* DEPLOYMENT COMMIT DETAILS */} +
+ + + {deployment.branch} + + + + + {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '} + {deployment.commitMessage} + + +
+ + {/* DEPLOYMENT INFOs */} +
+
+ + + {relativeTimeMs(deployment.createdAt)} + +
+ +
+ + + {formatAddress(deployment.createdBy.name ?? '')} + +
+
-
- - ^ {deployment.branch} - - - ^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '} - {deployment.commitMessage} - -
-
- - ^ {relativeTimeMs(deployment.createdAt)} ^{' '} - - {formatAddress(deployment.createdBy.name ?? '')} - - - - - - - - - - ^ Visit - - - setAssignDomainDialog(!assignDomainDialog)} - placeholder={''} - > - ^ Assign domain - - setChangeToProduction(!changeToProduction)} - disabled={!(deployment.environment !== Environment.Production)} - placeholder={''} - > - ^ Change to production - -
- setRedeployToProduction(!redeployToProduction)} - disabled={ - !( - deployment.environment === Environment.Production && - deployment.isCurrent - ) - } - placeholder={''} - > - ^ Redeploy to production - - setRollbackDeployment(!rollbackDeployment)} - disabled={ - deployment.isCurrent || - deployment.environment !== Environment.Production || - !Boolean(currentDeployment) - } - placeholder={''} - > - ^ Rollback to this version - -
-
-
- setChangeToProduction((preVal) => !preVal)} - open={changeToProduction} - confirmButtonTitle="Change" - color="blue" - handleConfirm={async () => { - await updateDeployment(); - setChangeToProduction((preVal) => !preVal); - }} - > -
- - Upon confirmation, this deployment will be changed to production. - - - - The new deployment will be associated with these domains: - - {prodBranchDomains.length > 0 && - prodBranchDomains.map((value) => { - return ( - - ^ {value.name} - - ); - })} -
-
- setRedeployToProduction((preVal) => !preVal)} - open={redeployToProduction} - confirmButtonTitle="Redeploy" - color="blue" - handleConfirm={async () => { - await redeployToProd(); - setRedeployToProduction((preVal) => !preVal); - }} - > -
- - Upon confirmation, new deployment will be created with the same - source code as current deployment. - - - - These domains will point to your new deployment: - - {deployment.domain?.name && ( - - {deployment.domain?.name} - - )} -
-
- {Boolean(currentDeployment) && ( - setRollbackDeployment((preVal) => !preVal)} - open={rollbackDeployment} - confirmButtonTitle="Rollback" - color="blue" - handleConfirm={async () => { - await rollbackDeploymentHandler(); - setRollbackDeployment((preVal) => !preVal); - }} - > -
- - Upon confirmation, this deployment will replace your current - deployment - - - - - These domains will point to your new deployment: - - - ^ {currentDeployment.domain?.name} - -
-
- )} - setAssignDomainDialog(!assignDomainDialog)} - />
); }; diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx new file mode 100644 index 0000000..f4eb980 --- /dev/null +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx @@ -0,0 +1,268 @@ +import React, { useState } from 'react'; +import toast from 'react-hot-toast'; +import { Deployment, Domain, Environment, Project } from 'gql-client'; +import { Button } from 'components/shared/Button'; +import { + GlobeIcon, + HorizontalDotIcon, + LinkIcon, + RefreshIcon, + RocketIcon, + UndoIcon, +} from 'components/shared/CustomIcon'; +import { + Menu, + MenuHandler, + MenuItem, + MenuList, +} from '@material-tailwind/react'; +import { ComponentPropsWithRef } from 'react'; +import ConfirmDialog from '../../../shared/ConfirmDialog'; +import AssignDomainDialog from './AssignDomainDialog'; +import DeploymentDialogBodyCard from './DeploymentDialogBodyCard'; +import { Typography } from '@material-tailwind/react'; +import { useGQLClient } from '../../../../context/GQLClientContext'; +import { cn } from 'utils/classnames'; + +interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> { + deployment: Deployment; + currentDeployment: Deployment; + onUpdate: () => Promise; + project: Project; + prodBranchDomains: Domain[]; +} + +export const DeploymentMenu = ({ + deployment, + currentDeployment, + onUpdate, + project, + prodBranchDomains, + className, + ...props +}: DeploymentMenuProps) => { + const client = useGQLClient(); + + const [changeToProduction, setChangeToProduction] = useState(false); + const [redeployToProduction, setRedeployToProduction] = useState(false); + const [rollbackDeployment, setRollbackDeployment] = useState(false); + const [assignDomainDialog, setAssignDomainDialog] = useState(false); + + const updateDeployment = async () => { + const isUpdated = await client.updateDeploymentToProd(deployment.id); + if (isUpdated) { + await onUpdate(); + toast.success('Deployment changed to production'); + } else { + toast.error('Unable to change deployment to production'); + } + }; + + const redeployToProd = async () => { + const isRedeployed = await client.redeployToProd(deployment.id); + if (isRedeployed) { + await onUpdate(); + toast.success('Redeployed to production'); + } else { + toast.error('Unable to redeploy to production'); + } + }; + + const rollbackDeploymentHandler = async () => { + const isRollbacked = await client.rollbackDeployment( + project.id, + deployment.id, + ); + if (isRollbacked) { + await onUpdate(); + toast.success('Deployment rolled back'); + } else { + toast.error('Unable to rollback deployment'); + } + }; + + return ( + <> +
+ + + +
+ {/* Dialogs */} + setChangeToProduction((preVal) => !preVal)} + open={changeToProduction} + confirmButtonTitle="Change" + color="blue" + handleConfirm={async () => { + await updateDeployment(); + setChangeToProduction((preVal) => !preVal); + }} + > +
+ + Upon confirmation, this deployment will be changed to production. + + + + The new deployment will be associated with these domains: + + {prodBranchDomains.length > 0 && + prodBranchDomains.map((value) => { + return ( + + ^ {value.name} + + ); + })} +
+
+ setRedeployToProduction((preVal) => !preVal)} + open={redeployToProduction} + confirmButtonTitle="Redeploy" + color="blue" + handleConfirm={async () => { + await redeployToProd(); + setRedeployToProduction((preVal) => !preVal); + }} + > +
+ + Upon confirmation, new deployment will be created with the same + source code as current deployment. + + + + These domains will point to your new deployment: + + {deployment.domain?.name && ( + + {deployment.domain?.name} + + )} +
+
+ {Boolean(currentDeployment) && ( + setRollbackDeployment((preVal) => !preVal)} + open={rollbackDeployment} + confirmButtonTitle="Rollback" + color="blue" + handleConfirm={async () => { + await rollbackDeploymentHandler(); + setRollbackDeployment((preVal) => !preVal); + }} + > +
+ + Upon confirmation, this deployment will replace your current + deployment + + + + + These domains will point to your new deployment: + + + ^ {currentDeployment.domain?.name} + +
+
+ )} + setAssignDomainDialog(!assignDomainDialog)} + /> + + ); +}; diff --git a/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx b/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx index c28c621..dd549c5 100644 --- a/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx +++ b/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx @@ -44,7 +44,7 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => { }, [value]); return ( -
+
{ return ( -
+
Activity
- - +
@@ -96,9 +107,6 @@ const Id = () => { Deployments - - Database - Integrations diff --git a/packages/frontend/src/pages/org-slug/projects/create/Template.tsx b/packages/frontend/src/pages/org-slug/projects/create/Template.tsx index 7309224..b8ee2f8 100644 --- a/packages/frontend/src/pages/org-slug/projects/create/Template.tsx +++ b/packages/frontend/src/pages/org-slug/projects/create/Template.tsx @@ -6,11 +6,11 @@ import { useSearchParams, } from 'react-router-dom'; -import Stepper from '../../../../components/Stepper'; import templates from '../../../../assets/templates'; import { Avatar } from 'components/shared/Avatar'; import { LinkChainIcon } from 'components/shared/CustomIcon'; import { Heading } from 'components/shared/Heading'; +import { Steps } from 'components/shared/Steps'; // TODO: Set dynamic route for template and load details from DB const CreateWithTemplate = () => { @@ -68,7 +68,7 @@ const CreateWithTemplate = () => {
- +
diff --git a/packages/frontend/src/pages/org-slug/projects/create/index.tsx b/packages/frontend/src/pages/org-slug/projects/create/index.tsx index 1c86ee3..49eeed9 100644 --- a/packages/frontend/src/pages/org-slug/projects/create/index.tsx +++ b/packages/frontend/src/pages/org-slug/projects/create/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import templates from 'assets/templates'; -import RepositoryList from 'components/projects/create/RepositoryList'; +import { RepositoryList } from 'components/projects/create/RepositoryList'; import ConnectAccount from 'components/projects/create/ConnectAccount'; import { useOctokit } from 'context/OctokitContext'; import { Heading } from 'components/shared/Heading'; @@ -13,8 +13,8 @@ const NewProject = () => { return isAuth ? ( <>
- - Start with template + + Start with a template
{templates.map((template) => { @@ -28,7 +28,7 @@ const NewProject = () => { })}
- + Import a repository diff --git a/packages/frontend/src/pages/org-slug/projects/create/layout.tsx b/packages/frontend/src/pages/org-slug/projects/create/layout.tsx index 3dad337..906a621 100644 --- a/packages/frontend/src/pages/org-slug/projects/create/layout.tsx +++ b/packages/frontend/src/pages/org-slug/projects/create/layout.tsx @@ -6,6 +6,7 @@ import { WavyBorder } from 'components/shared/WavyBorder'; import { Button } from 'components/shared/Button'; import { CrossIcon } from 'components/shared/CustomIcon'; import { cn } from 'utils/classnames'; +import * as Dialog from '@radix-ui/react-dialog'; export interface CreateProjectLayoutProps extends ComponentPropsWithoutRef<'section'> {} @@ -16,24 +17,77 @@ export const CreateProjectLayout = ({ }: CreateProjectLayoutProps) => { const { orgSlug } = useParams(); + const closeBtnLink = `/${orgSlug}`; + + const heading = ( + + Create new project + + ); + return ( -
-
-
- - Create new project - - - - + <> + {/* Desktop */} +
-
- + +
+ +
-
+ + {/* Mobile */} + {/* Setting modal={false} so even if modal is active on desktop, it doesn't block clicks */} + + + {/* Not using since modal={false} disables it and its content will not show */} +
+ + {/* Heading */} +
+ {heading} + + +
+ + {/* Border */} + + + {/* Page content */} +
+ +
+
+
+
+
+ ); }; diff --git a/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx b/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx index d26a2dd..d5f0346 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx @@ -88,17 +88,17 @@ const DeploymentsTabPanel = () => { setFilterValue(DEFAULT_FILTER_VALUE); }, []); - const onUpdateDeploymenToProd = async () => { + const onUpdateDeploymentToProd = async () => { await fetchDeployments(); }; return ( -
+
setFilterValue(value)} /> -
+
{Boolean(filteredDeployments.length) ? ( filteredDeployments.map((deployment, key) => { return ( @@ -106,7 +106,7 @@ const DeploymentsTabPanel = () => { deployment={deployment} key={key} currentDeployment={currentDeployment!} - onUpdate={onUpdateDeploymenToProd} + onUpdate={onUpdateDeploymentToProd} project={project} prodBranchDomains={prodBranchDomains} /> diff --git a/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx b/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx index 81cdbe1..c133704 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx @@ -115,8 +115,8 @@ const OverviewTabPanel = () => { }, [project]); return ( -
-
+
+
{ imageSrc={project.icon} type="blue" /> -
- +
+ {project.name} - +

{project.subDomain} - +

}> diff --git a/packages/frontend/tailwind.config.js b/packages/frontend/tailwind.config.js index c362e89..b5fa02b 100644 --- a/packages/frontend/tailwind.config.js +++ b/packages/frontend/tailwind.config.js @@ -12,6 +12,9 @@ export default withMT({ zIndex: { tooltip: '52', }, + letterSpacing: { + tight: '-0.084px', + }, fontFamily: { sans: ['Inter', 'sans-serif'], display: ['Inter Display', 'sans-serif'], @@ -84,6 +87,7 @@ export default withMT({ 900: '#0a3a5c', }, base: { + canvas: '#ECF6FE', bg: '#ffffff', 'bg-alternate': '#f8fafc', 'bg-emphasized': '#f1f5f9', diff --git a/yarn.lock b/yarn.lock index 60ab175..2708695 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3786,6 +3786,27 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-dialog@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300" + integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-focus-guards" "1.0.1" + "@radix-ui/react-focus-scope" "1.0.4" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-controllable-state" "1.0.1" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.5" + "@radix-ui/react-direction@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b" @@ -18188,7 +18209,7 @@ use-sync-external-store@1.2.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== -usehooks-ts@^2.10.0: +usehooks-ts@^2.15.1: version "2.15.1" resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-2.15.1.tgz#ede348c6f01b4b4fe981e240551624885a2fed83" integrity sha512-AK29ODCt4FT9XleILNbkbjjmkRCNaQrgxQEkvqHjlnT76iPXzTFGvK2Y/s83JEdSxRp43YEnSa3bYBEV6HZ26Q==