diff --git a/.vscode/settings.json b/.vscode/settings.json index f3e08235..c831fde1 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/build-webapp.sh b/build-webapp.sh index b22dde5d..a77ec641 100755 --- a/build-webapp.sh +++ b/build-webapp.sh @@ -9,14 +9,6 @@ if [[ -d "$DEST_DIR" ]]; then exit 1 fi -if [[ -f "$PKG_DIR/.env" ]]; then - echo "Using existing .env file" -else - mv "$PKG_DIR/.env.example" "$PKG_DIR/.env" - echo "Created .env file. Please populate with the correct values." - exit 1 -fi - cat > $PKG_DIR/.env < 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 d204537c..5211585c 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 eae6929f..80941346 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 693223bb..8d2a8e8b 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 04c5d6b5..d2976852 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 2c6410df..4eeaf9a5 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: 1000000 + gas: 1200000 fees: 200000aphoton - userKey: 0524fc22ea0a12e6c5cc4cfe08e73c95dffd0ab5ed72a59f459ed33134fa3b16 - bondId: 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac diff --git a/packages/deployer/deploy-frontend.sh b/packages/deployer/deploy-frontend.sh index 1e56f47a..92510428 100755 --- a/packages/deployer/deploy-frontend.sh +++ b/packages/deployer/deploy-frontend.sh @@ -1,11 +1,67 @@ #!/bin/bash +# Repository URL +REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base" + +# Get the latest commit hash from the repository +LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}') + +# Extract version from ../frontend/package.json +PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json) + +# Current date and time for note +CURRENT_DATE_TIME=$(date -u) + +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 +cat > ./records/application-deployment-request.yml < ./records/application-record.yml < { +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 4e70f9b0..239b1197 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 67698959..a251773d 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 1aae9c0c..71283ddb 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,110 @@ 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/TemplateCard/TemplateCard.tsx b/packages/frontend/src/components/projects/create/TemplateCard/TemplateCard.tsx index b7c549fd..ad073c2e 100644 --- a/packages/frontend/src/components/projects/create/TemplateCard/TemplateCard.tsx +++ b/packages/frontend/src/components/projects/create/TemplateCard/TemplateCard.tsx @@ -1,3 +1,4 @@ +import React, { ComponentPropsWithoutRef, useCallback } from 'react'; import { Button } from 'components/shared/Button'; import { ArrowRightCircleIcon, @@ -6,8 +7,7 @@ import { TemplateIconType, } from 'components/shared/CustomIcon'; import { Tag } from 'components/shared/Tag'; -import React, { ComponentPropsWithoutRef, useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { useToast } from 'components/shared/Toast'; import { cn } from 'utils/classnames'; @@ -24,9 +24,13 @@ export interface TemplateCardProps extends ComponentPropsWithoutRef<'div'> { isGitAuth: boolean; } -export const TemplateCard = ({ template, isGitAuth }: TemplateCardProps) => { +export const TemplateCard: React.FC = ({ + template, + isGitAuth, +}: TemplateCardProps) => { const { toast, dismiss } = useToast(); const navigate = useNavigate(); + const { orgSlug } = useParams(); const handleClick = useCallback(() => { if (template?.isComingSoon) { @@ -38,7 +42,9 @@ export const TemplateCard = ({ template, isGitAuth }: TemplateCardProps) => { }); } if (isGitAuth) { - return navigate(`/template?templateId=${template.id}`); + return navigate( + `/${orgSlug}/projects/create/template?templateId=${template.id}`, + ); } return toast({ id: 'connect-git-account', @@ -46,7 +52,7 @@ export const TemplateCard = ({ template, isGitAuth }: TemplateCardProps) => { variant: 'error', onDismiss: dismiss, }); - }, [isGitAuth, navigate, template, toast]); + }, [orgSlug, dismiss, isGitAuth, navigate, template, toast]); return ( - - - - - ^ 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 00000000..f4eb9808 --- /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/shared/CustomIcon/CommitIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CommitIcon.tsx new file mode 100644 index 00000000..38bb05f4 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CommitIcon.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const CommitIcon = (props: CustomIconProps) => { + return ( + + + + + + + + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/CopyIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CopyIcon.tsx new file mode 100644 index 00000000..34975bec --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CopyIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const CopyIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/LoaderIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/LoaderIcon.tsx new file mode 100644 index 00000000..720bfc22 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/LoaderIcon.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const LoaderIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/MinusCircleIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/MinusCircleIcon.tsx new file mode 100644 index 00000000..7f685677 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/MinusCircleIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const MinusCircleIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/RocketIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/RocketIcon.tsx new file mode 100644 index 00000000..488fba8f --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/RocketIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const RocketIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/UndoIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/UndoIcon.tsx new file mode 100644 index 00000000..713a0b6e --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/UndoIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const UndoIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/index.ts b/packages/frontend/src/components/shared/CustomIcon/index.ts index 4b8e4b18..9141aa17 100644 --- a/packages/frontend/src/components/shared/CustomIcon/index.ts +++ b/packages/frontend/src/components/shared/CustomIcon/index.ts @@ -43,6 +43,13 @@ export * from './LinkIcon'; export * from './CursorBoxIcon'; export * from './CrossCircleIcon'; export * from './RefreshIcon'; +export * from './CommitIcon'; +export * from './RocketIcon'; +export * from './RefreshIcon'; +export * from './UndoIcon'; +export * from './LoaderIcon'; +export * from './MinusCircleIcon'; +export * from './CopyIcon'; // Templates export * from './templates'; diff --git a/packages/frontend/src/components/shared/OverflownText/OverflownText.tsx b/packages/frontend/src/components/shared/OverflownText/OverflownText.tsx new file mode 100644 index 00000000..b03ea580 --- /dev/null +++ b/packages/frontend/src/components/shared/OverflownText/OverflownText.tsx @@ -0,0 +1,71 @@ +import { cn } from 'utils/classnames'; +import { Tooltip, TooltipProps } from 'components/shared/Tooltip'; +import { debounce } from 'lodash'; +import React, { + ComponentPropsWithRef, + PropsWithChildren, + useRef, + useState, + useEffect, +} from 'react'; +import { PolymorphicProps } from 'types/common'; + +interface OverflownTextProps extends ComponentPropsWithRef<'span'> { + tooltipProps?: TooltipProps; + content?: string; +} + +type ElementType = 'span' | 'div'; + +// This component is used to truncate text and show a tooltip if the text is overflown. +export const OverflownText = ({ + tooltipProps, + children, + content, + className, + as, + ...props +}: PropsWithChildren>) => { + const ref = useRef(null); + const [isOverflown, setIsOverflown] = useState(false); + + useEffect(() => { + const element = ref.current as HTMLElement | null; + if (!element) return; + + setIsOverflown(element.scrollWidth > element.clientWidth); + + const handleResize = () => { + const isOverflown = element.scrollWidth > element.clientWidth; + setIsOverflown(isOverflown); + }; + + window.addEventListener('resize', debounce(handleResize, 500)); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + const Component = as || 'span'; + + return ( + + + {children} + + + ); +}; diff --git a/packages/frontend/src/components/shared/OverflownText/index.ts b/packages/frontend/src/components/shared/OverflownText/index.ts new file mode 100644 index 00000000..1ffb16f2 --- /dev/null +++ b/packages/frontend/src/components/shared/OverflownText/index.ts @@ -0,0 +1 @@ +export * from './OverflownText'; diff --git a/packages/frontend/src/components/shared/Tag/Tag.theme.ts b/packages/frontend/src/components/shared/Tag/Tag.theme.ts index a027185d..884f79bc 100644 --- a/packages/frontend/src/components/shared/Tag/Tag.theme.ts +++ b/packages/frontend/src/components/shared/Tag/Tag.theme.ts @@ -4,7 +4,7 @@ import type { VariantProps } from 'tailwind-variants'; export const tagTheme = tv( { slots: { - wrapper: ['flex', 'gap-1.5', 'rounded-lg', 'border'], + wrapper: ['inline-flex', 'gap-1.5', 'rounded-lg', 'border'], icon: [], label: ['font-inter', 'text-xs'], }, 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 3dad337a..906a6215 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 8ae231d9..25be67b9 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx @@ -95,7 +95,7 @@ const DeploymentsTabPanel = () => { setFilterValue(DEFAULT_FILTER_VALUE); }, []); - const onUpdateDeploymenToProd = async () => { + const onUpdateDeploymentToProd = async () => { await fetchDeployments(); }; @@ -113,7 +113,7 @@ const DeploymentsTabPanel = () => { deployment={deployment} key={key} currentDeployment={currentDeployment!} - onUpdate={onUpdateDeploymenToProd} + onUpdate={onUpdateDeploymentToProd} project={project} prodBranchDomains={prodBranchDomains} /> diff --git a/packages/frontend/tailwind.config.js b/packages/frontend/tailwind.config.js index c362e898..b5fa02b0 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 60ab1754..e44f8793 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"