diff --git a/packages/frontend/src/components/projects/Dialog/CancelDeploymentDialog.tsx b/packages/frontend/src/components/projects/Dialog/CancelDeploymentDialog.tsx new file mode 100644 index 0000000..f238788 --- /dev/null +++ b/packages/frontend/src/components/projects/Dialog/CancelDeploymentDialog.tsx @@ -0,0 +1,30 @@ +import ConfirmDialog, { + ConfirmDialogProps, +} from 'components/shared/ConfirmDialog'; +import React from 'react'; + +interface CancelDeploymentDialogProps extends ConfirmDialogProps {} + +export const CancelDeploymentDialog = ({ + open, + handleCancel, + handleConfirm, + ...props +}: CancelDeploymentDialogProps) => { + return ( + +

+ This will halt the deployment and you'll have to start the process + from scratch. +

+
+ ); +}; diff --git a/packages/frontend/src/components/projects/Dialog/ChangeStateToProductionDialog.tsx b/packages/frontend/src/components/projects/Dialog/ChangeStateToProductionDialog.tsx new file mode 100644 index 0000000..cf7f1ef --- /dev/null +++ b/packages/frontend/src/components/projects/Dialog/ChangeStateToProductionDialog.tsx @@ -0,0 +1,90 @@ +import ConfirmDialog, { + ConfirmDialogProps, +} from 'components/shared/ConfirmDialog'; +import { Deployment, Domain } from 'gql-client'; +import React from 'react'; +import DeploymentDialogBodyCard from 'components/projects/project/deployments/DeploymentDialogBodyCard'; +import { Button } from 'components/shared/Button'; +import { + ChevronDoubleDownIcon, + LinkChainIcon, +} from 'components/shared/CustomIcon'; +import { TagProps } from 'components/shared/Tag'; + +interface ChangeStateToProductionDialogProps extends ConfirmDialogProps { + deployment: Deployment; + newDeployment?: Deployment; + domains: Domain[]; +} + +export const ChangeStateToProductionDialog = ({ + deployment, + newDeployment, + domains, + open, + handleCancel, + handleConfirm, + ...props +}: ChangeStateToProductionDialogProps) => { + const currentChip = { + value: 'Live Deployment', + type: 'positive' as TagProps['type'], + }; + const newChip = { + value: 'New Deployment', + type: 'attention' as TagProps['type'], + }; + + return ( + +
+
+

+ Upon confirmation, this deployment will be changed to production. +

+ + {newDeployment && ( + <> +
+ {Array.from({ length: 7 }).map((_, index) => ( + + ))} +
+ + + )} +
+
+

+ The new deployment will be associated with these domains: +

+ {domains.length > 0 && + domains.map((value) => { + return ( + + ); + })} +
+
+
+ ); +}; diff --git a/packages/frontend/src/components/projects/Dialog/DeleteDomainDialog.tsx b/packages/frontend/src/components/projects/Dialog/DeleteDomainDialog.tsx new file mode 100644 index 0000000..5d6d3e0 --- /dev/null +++ b/packages/frontend/src/components/projects/Dialog/DeleteDomainDialog.tsx @@ -0,0 +1,42 @@ +import ConfirmDialog, { + ConfirmDialogProps, +} from 'components/shared/ConfirmDialog'; +import React from 'react'; + +interface DeleteDomainDialogProps extends ConfirmDialogProps { + projectName: string; + domainName: string; +} + +export const DeleteDomainDialog = ({ + projectName, + domainName, + open, + handleCancel, + handleConfirm, + ...props +}: DeleteDomainDialogProps) => { + return ( + +

+ Once deleted, the project{' '} + + {projectName} + {' '} + will not be accessible from the domain{' '} + + {domainName} + + . +

+
+ ); +}; diff --git a/packages/frontend/src/components/projects/Dialog/DeleteVariableDialog.tsx b/packages/frontend/src/components/projects/Dialog/DeleteVariableDialog.tsx new file mode 100644 index 0000000..227fa34 --- /dev/null +++ b/packages/frontend/src/components/projects/Dialog/DeleteVariableDialog.tsx @@ -0,0 +1,36 @@ +import ConfirmDialog, { + ConfirmDialogProps, +} from 'components/shared/ConfirmDialog'; +import React from 'react'; + +interface DeleteVariableDialogProps extends ConfirmDialogProps { + variableKey: string; +} + +export const DeleteVariableDialog = ({ + variableKey, + open, + handleCancel, + handleConfirm, + ...props +}: DeleteVariableDialogProps) => { + return ( + +

+ Are you sure you want to delete the variable{' '} + + {variableKey} + + ? +

+
+ ); +}; diff --git a/packages/frontend/src/components/projects/Dialog/DeleteWebhookDialog.tsx b/packages/frontend/src/components/projects/Dialog/DeleteWebhookDialog.tsx new file mode 100644 index 0000000..c2c82c1 --- /dev/null +++ b/packages/frontend/src/components/projects/Dialog/DeleteWebhookDialog.tsx @@ -0,0 +1,36 @@ +import ConfirmDialog, { + ConfirmDialogProps, +} from 'components/shared/ConfirmDialog'; +import React from 'react'; + +interface DeleteWebhookDialogProps extends ConfirmDialogProps { + webhookUrl: string; +} + +export const DeleteWebhookDialog = ({ + webhookUrl, + open, + handleCancel, + handleConfirm, + ...props +}: DeleteWebhookDialogProps) => { + return ( + +

+ Are you sure you want to delete{' '} + + {webhookUrl} + + ? +

+
+ ); +}; diff --git a/packages/frontend/src/components/projects/Dialog/DisconnectRepositoryDialog.tsx b/packages/frontend/src/components/projects/Dialog/DisconnectRepositoryDialog.tsx new file mode 100644 index 0000000..52ac52f --- /dev/null +++ b/packages/frontend/src/components/projects/Dialog/DisconnectRepositoryDialog.tsx @@ -0,0 +1,30 @@ +import ConfirmDialog, { + ConfirmDialogProps, +} from 'components/shared/ConfirmDialog'; +import React from 'react'; + +interface DisconnectRepositoryDialogProps extends ConfirmDialogProps {} + +export const DisconnectRepositoryDialog = ({ + open, + handleCancel, + handleConfirm, + ...props +}: DisconnectRepositoryDialogProps) => { + return ( + +

+ Any data tied to your Git project may become misconfigured. Are you sure + you want to continue? +

+
+ ); +}; diff --git a/packages/frontend/src/components/projects/Dialog/RemoveMemberDialog.tsx b/packages/frontend/src/components/projects/Dialog/RemoveMemberDialog.tsx new file mode 100644 index 0000000..6695ff8 --- /dev/null +++ b/packages/frontend/src/components/projects/Dialog/RemoveMemberDialog.tsx @@ -0,0 +1,38 @@ +import ConfirmDialog, { + ConfirmDialogProps, +} from 'components/shared/ConfirmDialog'; +import React from 'react'; +import { formatAddress } from 'utils/format'; + +interface RemoveMemberDialogProps extends ConfirmDialogProps { + memberName: string; + ethAddress: string; + emailDomain: string; +} + +export const RemoveMemberDialog = ({ + memberName, + ethAddress, + emailDomain, + open, + handleCancel, + handleConfirm, + ...props +}: RemoveMemberDialogProps) => { + return ( + +

+ Once removed, {formatAddress(memberName)} ({formatAddress(ethAddress)}@ + {emailDomain}) will not be able to access this project. +

+
+ ); +}; diff --git a/packages/frontend/src/components/projects/Dialog/TransferProjectDialog.tsx b/packages/frontend/src/components/projects/Dialog/TransferProjectDialog.tsx new file mode 100644 index 0000000..7d26ab9 --- /dev/null +++ b/packages/frontend/src/components/projects/Dialog/TransferProjectDialog.tsx @@ -0,0 +1,47 @@ +import ConfirmDialog, { + ConfirmDialogProps, +} from 'components/shared/ConfirmDialog'; +import React from 'react'; + +interface TransferProjectDialogProps extends ConfirmDialogProps { + projectName: string; + from: string; + to: string; +} + +export const TransferProjectDialog = ({ + projectName, + from, + to, + open, + handleCancel, + handleConfirm, + ...props +}: TransferProjectDialogProps) => { + return ( + +

+ Upon confirmation, your project{' '} + + {projectName} + {' '} + will be transferred from{' '} + + {from} + {' '} + to{' '} + + {to} + + . +

+
+ ); +}; diff --git a/packages/frontend/src/components/projects/ProjectSearchBar/ProjectSearchBarDialog.tsx b/packages/frontend/src/components/projects/ProjectSearchBar/ProjectSearchBarDialog.tsx index e49b44d..0f5e117 100644 --- a/packages/frontend/src/components/projects/ProjectSearchBar/ProjectSearchBarDialog.tsx +++ b/packages/frontend/src/components/projects/ProjectSearchBar/ProjectSearchBarDialog.tsx @@ -12,6 +12,7 @@ import { useNavigate } from 'react-router-dom'; import { useCombobox } from 'downshift'; interface ProjectSearchBarDialogProps extends Dialog.DialogProps { + open?: boolean; onClose?: () => void; onClickItem?: (data: Project) => void; } diff --git a/packages/frontend/src/components/projects/create/Deploy.tsx b/packages/frontend/src/components/projects/create/Deploy.tsx index a251773..f443d5e 100644 --- a/packages/frontend/src/components/projects/create/Deploy.tsx +++ b/packages/frontend/src/components/projects/create/Deploy.tsx @@ -1,14 +1,12 @@ import React, { useCallback, useEffect } from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; -import { Typography } from '@material-tailwind/react'; - import { DeployStep, DeployStatus } from './DeployStep'; -import { Stopwatch, setStopWatchOffset } from '../../StopWatch'; -import ConfirmDialog from 'components/shared/ConfirmDialog'; +import { Stopwatch, setStopWatchOffset } from 'components/StopWatch'; import { Heading } from 'components/shared/Heading'; import { Button } from 'components/shared/Button'; import { ClockOutlineIcon, WarningIcon } from 'components/shared/CustomIcon'; +import { CancelDeploymentDialog } from 'components/projects/Dialog/CancelDeploymentDialog'; const TIMEOUT_DURATION = 5000; const Deploy = () => { @@ -55,19 +53,11 @@ const Deploy = () => { > Cancel - - - This will halt the deployment and you will have to start the process - from scratch. - - + />
diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentDialogBodyCard.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentDialogBodyCard.tsx index a8b40b8..fd008de 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentDialogBodyCard.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentDialogBodyCard.tsx @@ -1,17 +1,23 @@ import React from 'react'; import { Deployment } from 'gql-client'; -import { Typography, Chip, Card } from '@material-tailwind/react'; -import { color } from '@material-tailwind/react/types/components/chip'; -import { relativeTimeMs } from '../../../../utils/time'; +import { relativeTimeMs } from 'utils/time'; import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants'; -import { formatAddress } from '../../../../utils/format'; +import { + BranchStrokeIcon, + ClockOutlineIcon, + CommitIcon, +} from 'components/shared/CustomIcon'; +import { Avatar } from 'components/shared/Avatar'; +import { getInitials } from 'utils/geInitials'; +import { OverflownText } from 'components/shared/OverflownText'; +import { Tag, TagProps } from 'components/shared/Tag'; interface DeploymentDialogBodyCardProps { deployment: Deployment; chip?: { value: string; - color?: color; + type?: TagProps['type']; }; } @@ -19,31 +25,54 @@ const DeploymentDialogBodyCard = ({ chip, deployment, }: DeploymentDialogBodyCardProps) => { + const commit = + deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH) + + ' ' + + deployment.commitMessage; + return ( - +
{chip && ( - + + {chip.value} + )} - {deployment.url && ( - +
+ {/* Title */} +

{deployment.url} - - )} - - ^ {deployment.branch} ^{' '} - {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '} - {deployment.commitMessage} - - - ^ {relativeTimeMs(deployment.createdAt)} ^{' '} - {formatAddress(deployment.createdBy.name ?? '')} - - +

+ {/* Branch & commit */} +
+
+ +

{deployment.branch}

+
+
+ +

+ {commit} +

+
+
+
+
+ +

+ {relativeTimeMs(deployment.createdAt)} +

+ +

+ {deployment.createdBy.name ?? 'Unknown'} +

+
+
); }; diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx index f4eb980..516b468 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx @@ -17,12 +17,10 @@ import { 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 { useGQLClient } from 'context/GQLClientContext'; import { cn } from 'utils/classnames'; +import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog'; interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> { deployment: Deployment; @@ -158,106 +156,44 @@ export const DeploymentMenu = ({
{/* Dialogs */} - setChangeToProduction((preVal) => !preVal)} - open={changeToProduction} confirmButtonTitle="Change" - color="blue" + handleCancel={() => setChangeToProduction((preVal) => !preVal)} + open={changeToProduction} 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)} + handleCancel={() => 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} - - )} -
-
+ deployment={deployment} + domains={deployment.domain ? [deployment.domain] : []} + /> {Boolean(currentDeployment) && ( - setRollbackDeployment((preVal) => !preVal)} + handleCancel={() => 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} - -
-
+ deployment={currentDeployment} + newDeployment={deployment} + domains={currentDeployment.domain ? [currentDeployment.domain] : []} + /> )} - setDeleteDialogOpen((preVal) => !preVal)} + setDeleteDialogOpen((preVal) => !preVal)} open={deleteDialogOpen} - confirmButtonTitle="Yes, Delete domain" handleConfirm={() => { deleteDomain(); setDeleteDialogOpen((preVal) => !preVal); }} - color="red" - > - - Once deleted, the project{' '} - - {project.name} - {' '} - will not be accessible from the domain{' '} - - {domain.name}. - - - + projectName={project.name} + domainName={domain.name} + /> diff --git a/packages/frontend/src/components/projects/project/settings/EditEnvironmentVariableRow.tsx b/packages/frontend/src/components/projects/project/settings/EditEnvironmentVariableRow.tsx index c15c142..0e38298 100644 --- a/packages/frontend/src/components/projects/project/settings/EditEnvironmentVariableRow.tsx +++ b/packages/frontend/src/components/projects/project/settings/EditEnvironmentVariableRow.tsx @@ -5,8 +5,8 @@ import { EnvironmentVariable } from 'gql-client'; import { IconButton, Input, Typography } from '@material-tailwind/react'; -import ConfirmDialog from '../../../shared/ConfirmDialog'; -import { useGQLClient } from '../../../../context/GQLClientContext'; +import { useGQLClient } from 'context/GQLClientContext'; +import { DeleteVariableDialog } from 'components/projects/Dialog/DeleteVariableDialog'; const ShowPasswordIcon = ({ handler, @@ -161,20 +161,12 @@ const EditEnvironmentVariableRow = ({ )} - - setDeleteDialogOpen((preVal) => !preVal)} + setDeleteDialogOpen((preVal) => !preVal)} open={deleteDialogOpen} - confirmButtonTitle="Yes, Confirm delete" handleConfirm={removeEnvironmentVariableHandler} - color="red" - > - - Are you sure you want to delete the variable  - {variable.key}? - - + variableKey={variable.key} + /> ); }; diff --git a/packages/frontend/src/components/projects/project/settings/MemberCard.tsx b/packages/frontend/src/components/projects/project/settings/MemberCard.tsx index 715d8bf..8dd8fd1 100644 --- a/packages/frontend/src/components/projects/project/settings/MemberCard.tsx +++ b/packages/frontend/src/components/projects/project/settings/MemberCard.tsx @@ -3,15 +3,14 @@ import { Permission, User } from 'gql-client'; import { Select, - Typography, Option, Chip, IconButton, Tooltip, } from '@material-tailwind/react'; -import ConfirmDialog from '../../../shared/ConfirmDialog'; -import { formatAddress } from '../../../../utils/format'; +import { formatAddress } from 'utils/format'; +import { RemoveMemberDialog } from 'components/projects/Dialog/RemoveMemberDialog'; const PERMISSION_OPTIONS = [ { @@ -141,25 +140,19 @@ const MemberCard = ({ )} - setRemoveMemberDialogOpen((preVal) => !preVal)} + setRemoveMemberDialogOpen((preVal) => !preVal)} open={removeMemberDialogOpen} - confirmButtonTitle="Yes, Remove member" handleConfirm={() => { setRemoveMemberDialogOpen((preVal) => !preVal); if (onRemoveProjectMember) { onRemoveProjectMember(); } }} - color="red" - > - - Once removed, {formatAddress(member.name ?? '')} ( - {formatAddress(ethAddress)}@{emailDomain}) will not be able to access - this project. - - + memberName={member.name ?? ''} + ethAddress={ethAddress} + emailDomain={emailDomain} + /> ); }; diff --git a/packages/frontend/src/components/projects/project/settings/RepoConnectedSection.tsx b/packages/frontend/src/components/projects/project/settings/RepoConnectedSection.tsx index 594062f..f0a35e9 100644 --- a/packages/frontend/src/components/projects/project/settings/RepoConnectedSection.tsx +++ b/packages/frontend/src/components/projects/project/settings/RepoConnectedSection.tsx @@ -2,8 +2,8 @@ import React, { useState } from 'react'; import { Button, Typography } from '@material-tailwind/react'; -import { GitRepositoryDetails } from '../../../../types'; -import ConfirmDialog from '../../../shared/ConfirmDialog'; +import { GitRepositoryDetails } from 'types'; +import { DisconnectRepositoryDialog } from 'components/projects/Dialog/DisconnectRepositoryDialog'; const RepoConnectedSection = ({ linkedRepo, @@ -34,21 +34,13 @@ const RepoConnectedSection = ({ ^ Disconnect - setDisconnectRepoDialogOpen((preVal) => !preVal)} + setDisconnectRepoDialogOpen((preVal) => !preVal)} open={disconnectRepoDialogOpen} - confirmButtonTitle="Yes, confirm disconnect" handleConfirm={() => { setDisconnectRepoDialogOpen((preVal) => !preVal); }} - color="red" - > - - Any data tied to your Git project may become misconfigured. Are you - sure you want to continue? - - + /> ); }; diff --git a/packages/frontend/src/components/projects/project/settings/WebhookCard.tsx b/packages/frontend/src/components/projects/project/settings/WebhookCard.tsx index 0d03e46..80aaf4f 100644 --- a/packages/frontend/src/components/projects/project/settings/WebhookCard.tsx +++ b/packages/frontend/src/components/projects/project/settings/WebhookCard.tsx @@ -1,9 +1,9 @@ import React, { useState } from 'react'; import toast from 'react-hot-toast'; -import { Button, Typography } from '@material-tailwind/react'; +import { Button } from '@material-tailwind/react'; -import ConfirmDialog from '../../../shared/ConfirmDialog'; +import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog'; interface WebhookCardProps { webhookUrl: string; @@ -15,7 +15,6 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => { return (
{webhookUrl} -
- - setDeleteDialogOpen((preVal) => !preVal)} + setDeleteDialogOpen((preVal) => !preVal)} open={deleteDialogOpen} - confirmButtonTitle="Yes, Confirm delete" handleConfirm={() => { setDeleteDialogOpen((preVal) => !preVal); onDelete(); }} - color="red" - > - - Are you sure you want to delete the variable{' '} - {webhookUrl}? - - + webhookUrl={webhookUrl} + />
); }; diff --git a/packages/frontend/src/components/shared/ConfirmDialog.tsx b/packages/frontend/src/components/shared/ConfirmDialog.tsx index 1de20e9..e7374ad 100644 --- a/packages/frontend/src/components/shared/ConfirmDialog.tsx +++ b/packages/frontend/src/components/shared/ConfirmDialog.tsx @@ -1,64 +1,52 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; +import { Modal, ModalProps } from './Modal'; +import { Button, ButtonOrLinkProps } from './Button'; -import { color } from '@material-tailwind/react/types/components/button'; -import { - Typography, - Button, - Dialog, - DialogHeader, - DialogBody, - DialogFooter, -} from '@material-tailwind/react'; - -type ConfirmDialogProp = { - children: React.ReactNode; - dialogTitle: string; +export type ConfirmDialogProps = ModalProps & { + children?: ReactNode; + dialogTitle?: string; open: boolean; - handleOpen: () => void; - confirmButtonTitle: string; + handleCancel: () => void; + confirmButtonTitle?: string; handleConfirm?: () => void; - color: color; + cancelButtonProps?: ButtonOrLinkProps; + confirmButtonProps?: ButtonOrLinkProps; }; const ConfirmDialog = ({ children, dialogTitle, - open, - handleOpen, + handleCancel, confirmButtonTitle, handleConfirm, - color, -}: ConfirmDialogProp) => { + cancelButtonProps, + confirmButtonProps, + ...props +}: ConfirmDialogProps) => { + // Close the dialog when the user clicks outside of it + const handleOpenChange = (open: boolean) => { + if (!open) return handleCancel?.(); + }; + return ( - - - - {dialogTitle}{' '} - - - - {children} - - - - - + + + {dialogTitle} + {children} + + + + + + ); }; diff --git a/packages/frontend/src/components/shared/CustomIcon/ChevronDoubleDownIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/ChevronDoubleDownIcon.tsx new file mode 100644 index 0000000..64993ac --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/ChevronDoubleDownIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const ChevronDoubleDownIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/index.ts b/packages/frontend/src/components/shared/CustomIcon/index.ts index 29e3844..4d77a8b 100644 --- a/packages/frontend/src/components/shared/CustomIcon/index.ts +++ b/packages/frontend/src/components/shared/CustomIcon/index.ts @@ -61,6 +61,7 @@ export * from './CirclePlaceholderOnIcon'; export * from './WarningTriangleIcon'; export * from './CheckRadioOutlineIcon'; export * from './TrendingIcon'; +export * from './ChevronDoubleDownIcon'; // Templates export * from './templates'; diff --git a/packages/frontend/src/components/shared/Modal/Modal.theme.ts b/packages/frontend/src/components/shared/Modal/Modal.theme.ts new file mode 100644 index 0000000..4552ff7 --- /dev/null +++ b/packages/frontend/src/components/shared/Modal/Modal.theme.ts @@ -0,0 +1,73 @@ +import type { VariantProps } from 'tailwind-variants'; +import { tv } from 'tailwind-variants'; + +export const modalTheme = tv({ + slots: { + overlay: [ + 'z-modal', + 'fixed', + 'inset-0', + 'bg-black/80', + 'backdrop-blur-sm', + 'overflow-y-auto', + 'flex', + 'justify-center', + 'items-end', + 'sm:items-center', + 'p-0', + 'sm:p-10', + 'data-[state=closed]:animate-[dialog-overlay-hide_200ms]', + 'data-[state=open]:animate-[dialog-overlay-show_200ms]', + 'data-[state=closed]:hidden', // Fix overlay not close when modal is closed + ], + close: ['absolute', 'right-4', 'top-2', 'sm:right-6', 'sm:top-3', 'z-[1]'], + header: [ + 'flex', + 'flex-col', + 'gap-4', + 'items-start', + 'px-4', + 'py-4', + 'sm:px-6', + 'sm:py-5', + 'bg-base-bg-alternate', + ], + headerTitle: [ + 'text-base', + 'sm:text-lg', + 'tracking-[0.011em]', + 'sm:tracking-normal', + 'text-elements-high-em', + ], + headerDescription: ['text-sm', 'text-elements-low-em'], + footer: ['flex', 'gap-3', 'px-4', 'pb-4', 'pt-7', 'sm:pb-6', 'sm:px-6'], + content: [ + 'h-fit', + 'sm:min-h-0', + 'sm:m-auto', + 'relative', + 'flex', + 'flex-col', + 'overflow-hidden', + 'w-full', + 'sm:max-w-[562px]', + 'rounded-2xl', + 'bg-base-bg', + 'shadow-card', + 'text-elements-high-em', + ], + body: ['flex-1', 'px-4', 'pt-4', 'sm:pt-6', 'sm:px-6'], + }, + variants: { + fullPage: { + true: { + content: ['h-full'], + overlay: ['!p-0'], + }, + }, + }, + defaultVariants: { + fullPage: false, + }, +}); +export type ModalVariants = VariantProps; diff --git a/packages/frontend/src/components/shared/Modal/Modal.tsx b/packages/frontend/src/components/shared/Modal/Modal.tsx new file mode 100644 index 0000000..ef9069f --- /dev/null +++ b/packages/frontend/src/components/shared/Modal/Modal.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import type { DialogProps } from '@radix-ui/react-dialog'; +import { Root, Trigger } from '@radix-ui/react-dialog'; +import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react'; + +import type { ModalVariants } from './Modal.theme'; +import { ModalBody } from './ModalBody'; +import { ModalContent } from './ModalContent'; +import { ModalFooter } from './ModalFooter'; +import { ModalHeader } from './ModalHeader'; +import ModalProvider from './ModalProvider'; + +export interface ModalProps + extends ComponentPropsWithoutRef<'div'>, + ModalVariants, + DialogProps { + hasCloseButton?: boolean; + hasOverlay?: boolean; + preventClickOutsideToClose?: boolean; +} +export const Modal = ({ + children, + hasCloseButton = true, + hasOverlay = true, + preventClickOutsideToClose = false, + fullPage = false, + ...props +}: PropsWithChildren) => { + return ( + + {children} + + ); +}; + +Modal.Trigger = Trigger; +Modal.Content = ModalContent; +Modal.Header = ModalHeader; +Modal.Footer = ModalFooter; +Modal.Body = ModalBody; diff --git a/packages/frontend/src/components/shared/Modal/ModalBody/ModalBody.tsx b/packages/frontend/src/components/shared/Modal/ModalBody/ModalBody.tsx new file mode 100644 index 0000000..3079b26 --- /dev/null +++ b/packages/frontend/src/components/shared/Modal/ModalBody/ModalBody.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react'; +import { modalTheme } from 'components/shared/Modal/Modal.theme'; + +export interface ModalBodyProps extends ComponentPropsWithoutRef<'div'> { + className?: string; +} + +export const ModalBody = ({ + children, + className, + ...props +}: PropsWithChildren) => { + const { body } = modalTheme(); + + return ( +
+ {children} +
+ ); +}; diff --git a/packages/frontend/src/components/shared/Modal/ModalBody/index.ts b/packages/frontend/src/components/shared/Modal/ModalBody/index.ts new file mode 100644 index 0000000..ad32ff0 --- /dev/null +++ b/packages/frontend/src/components/shared/Modal/ModalBody/index.ts @@ -0,0 +1 @@ +export * from './ModalBody'; diff --git a/packages/frontend/src/components/shared/Modal/ModalContent/ModalContent.tsx b/packages/frontend/src/components/shared/Modal/ModalContent/ModalContent.tsx new file mode 100644 index 0000000..166256a --- /dev/null +++ b/packages/frontend/src/components/shared/Modal/ModalContent/ModalContent.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import type { DialogContentProps } from '@radix-ui/react-dialog'; +import { Close, Content, Overlay, Portal } from '@radix-ui/react-dialog'; +import { Ref, forwardRef, type PropsWithChildren } from 'react'; +import { useModal } from 'components/shared/Modal/ModalProvider'; +import { modalTheme } from 'components/shared/Modal/Modal.theme'; +import { Button } from 'components/shared/Button'; +import { CrossIcon } from 'components/shared/CustomIcon'; + +type PointerDownOutsideEvent = CustomEvent<{ + originalEvent: PointerEvent; +}>; + +export interface ModalContentProps extends DialogContentProps { + className?: string; +} + +const ModalContent = forwardRef( + ( + { children, className, ...props }: PropsWithChildren, + forwardedRef, + ) => { + const { hasCloseButton, preventClickOutsideToClose, fullPage } = useModal(); + + const { content, close, overlay } = modalTheme({ fullPage }); + + const preventClickOutsideToCloseProps = preventClickOutsideToClose && { + onPointerDownOutside: (e: PointerDownOutsideEvent) => e.preventDefault(), + onEscapeKeyDown: (e: KeyboardEvent) => e.preventDefault(), + }; + + return ( + + + } + > + {hasCloseButton && ( + + + + )} + {children} + + + + ); + }, +); + +ModalContent.displayName = 'ModalContent'; + +export { ModalContent }; diff --git a/packages/frontend/src/components/shared/Modal/ModalContent/index.ts b/packages/frontend/src/components/shared/Modal/ModalContent/index.ts new file mode 100644 index 0000000..79dee45 --- /dev/null +++ b/packages/frontend/src/components/shared/Modal/ModalContent/index.ts @@ -0,0 +1 @@ +export * from './ModalContent'; diff --git a/packages/frontend/src/components/shared/Modal/ModalFooter/ModalFooter.tsx b/packages/frontend/src/components/shared/Modal/ModalFooter/ModalFooter.tsx new file mode 100644 index 0000000..5152e88 --- /dev/null +++ b/packages/frontend/src/components/shared/Modal/ModalFooter/ModalFooter.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react'; +import { modalTheme } from 'components/shared/Modal/Modal.theme'; + +type ModalFooterProps = ComponentPropsWithoutRef<'div'> & { + className?: string; +}; + +export const ModalFooter = ({ + children, + className, + ...props +}: PropsWithChildren) => { + const { footer } = modalTheme({ + className, + }); + + return ( +
+ {children} +
+ ); +}; diff --git a/packages/frontend/src/components/shared/Modal/ModalFooter/index.ts b/packages/frontend/src/components/shared/Modal/ModalFooter/index.ts new file mode 100644 index 0000000..2da6856 --- /dev/null +++ b/packages/frontend/src/components/shared/Modal/ModalFooter/index.ts @@ -0,0 +1 @@ +export * from './ModalFooter'; diff --git a/packages/frontend/src/components/shared/Modal/ModalHeader/ModalHeader.tsx b/packages/frontend/src/components/shared/Modal/ModalHeader/ModalHeader.tsx new file mode 100644 index 0000000..c078384 --- /dev/null +++ b/packages/frontend/src/components/shared/Modal/ModalHeader/ModalHeader.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import type { DialogDescriptionProps } from '@radix-ui/react-dialog'; +import { Description, Title } from '@radix-ui/react-dialog'; +import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react'; +import { Heading } from 'components/shared/Heading'; +import { modalTheme } from 'components/shared/Modal/Modal.theme'; +import { WavyBorder } from 'components/shared/WavyBorder'; + +type ModalHeaderProps = ComponentPropsWithoutRef<'div'> & { + className?: string; + description?: string | React.ReactNode; + descriptionProps?: DialogDescriptionProps; + headingProps?: ComponentPropsWithoutRef<'h2'>; +}; + +export const ModalHeader = ({ + children, + description, + className, + descriptionProps, + headingProps, + ...props +}: PropsWithChildren) => { + const { header, headerDescription, headerTitle } = modalTheme(); + + return ( + <> +
+ + <Heading + {...headingProps} + className={headerTitle({ className: headingProps?.className })} + > + {children} + </Heading> + + {description && ( + + {description} + + )} +
+ + + ); +}; diff --git a/packages/frontend/src/components/shared/Modal/ModalHeader/index.ts b/packages/frontend/src/components/shared/Modal/ModalHeader/index.ts new file mode 100644 index 0000000..4424e62 --- /dev/null +++ b/packages/frontend/src/components/shared/Modal/ModalHeader/index.ts @@ -0,0 +1 @@ +export * from './ModalHeader'; diff --git a/packages/frontend/src/components/shared/Modal/ModalProvider.tsx b/packages/frontend/src/components/shared/Modal/ModalProvider.tsx new file mode 100644 index 0000000..2be6beb --- /dev/null +++ b/packages/frontend/src/components/shared/Modal/ModalProvider.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import type { PropsWithChildren } from 'react'; +import { createContext, useContext } from 'react'; + +import type { ModalProps } from './Modal'; +import type { ModalVariants } from './Modal.theme'; + +export interface ModalProviderProps + extends Partial, + ModalProps {} + +type ModalProviderContext = ReturnType; + +const ModalContext = createContext | undefined>( + undefined, +); + +// For inferring return type +const useModalValues = (props: ModalProviderProps) => { + return props; +}; + +export const ModalProvider = ({ + children, + ...props +}: PropsWithChildren): JSX.Element => { + const values = useModalValues(props); + + return ( + {children} + ); +}; + +export const useModal = () => { + const context = useContext(ModalContext); + if (context === undefined) { + throw new Error('useModal was used outside of its Provider'); + } + return context; +}; + +export default ModalProvider; diff --git a/packages/frontend/src/components/shared/Modal/index.ts b/packages/frontend/src/components/shared/Modal/index.ts new file mode 100644 index 0000000..cb89ee1 --- /dev/null +++ b/packages/frontend/src/components/shared/Modal/index.ts @@ -0,0 +1 @@ +export * from './Modal'; diff --git a/packages/frontend/src/components/shared/Tag/Tag.tsx b/packages/frontend/src/components/shared/Tag/Tag.tsx index 8be34c8..08fb665 100644 --- a/packages/frontend/src/components/shared/Tag/Tag.tsx +++ b/packages/frontend/src/components/shared/Tag/Tag.tsx @@ -6,7 +6,7 @@ import React, { import { tagTheme, type TagTheme } from './Tag.theme'; import { cloneIcon } from 'utils/cloneIcon'; -type TagProps = ComponentPropsWithoutRef<'div'> & +export type TagProps = ComponentPropsWithoutRef<'div'> & TagTheme & { /** * The optional left icon element for a component. diff --git a/packages/frontend/src/index.css b/packages/frontend/src/index.css index 502904a..541fb61 100644 --- a/packages/frontend/src/index.css +++ b/packages/frontend/src/index.css @@ -153,4 +153,44 @@ .focus-ring { @apply focus-visible:ring-[3px] focus-visible:ring-snowball-200 focus-visible:ring-offset-1 focus-visible:ring-offset-snowball-500 focus-visible:outline-none; } + + @keyframes dialog-overlay-show { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes dialog-overlay-hide { + from { + opacity: 1; + } + to { + opacity: 0; + } + } + + @keyframes dialog-content-show { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.95); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + } + + @keyframes dialog-content-hide { + from { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + to { + opacity: 0; + transform: translate(-50%, -50%) scale(0.95); + } + } } diff --git a/packages/frontend/src/layouts/ProjectSearch.tsx b/packages/frontend/src/layouts/ProjectSearch.tsx index 5a71356..1343a5b 100644 --- a/packages/frontend/src/layouts/ProjectSearch.tsx +++ b/packages/frontend/src/layouts/ProjectSearch.tsx @@ -72,7 +72,7 @@ const ProjectSearch = () => { {/* Content */} -
+
diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index feee459..84e3e18 100644 --- a/packages/frontend/src/pages/components/index.tsx +++ b/packages/frontend/src/pages/components/index.tsx @@ -35,6 +35,8 @@ import { import { renderDefaultTag, renderMinimalTag } from './renders/tag'; import { renderToast, renderToastsWithCta } from './renders/toast'; import { renderTooltips } from './renders/tooltip'; +import { Button } from 'components/shared/Button'; +import { Modal } from 'components/shared/Modal'; const Page: React.FC = () => { const [singleDate, setSingleDate] = useState(); @@ -57,6 +59,32 @@ const Page: React.FC = () => {
+ {/* Modal */} +
+
+

Modal

+
+ {/* Modal example */} + + + + + + Modal title + +

Modal content

+
+ + + +
+
+
+
+
+ +
+ {/* Steps */}
diff --git a/packages/frontend/src/pages/components/modals.tsx b/packages/frontend/src/pages/components/modals.tsx new file mode 100644 index 0000000..63d34e2 --- /dev/null +++ b/packages/frontend/src/pages/components/modals.tsx @@ -0,0 +1,274 @@ +import React from 'react'; + +import { useState } from 'react'; + +import { Button } from 'components/shared/Button'; +import { Modal } from 'components/shared/Modal'; +import { TransferProjectDialog } from 'components/projects/Dialog/TransferProjectDialog'; +import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog'; +import { DisconnectRepositoryDialog } from 'components/projects/Dialog/DisconnectRepositoryDialog'; +import { RemoveMemberDialog } from 'components/projects/Dialog/RemoveMemberDialog'; +import { DeleteVariableDialog } from 'components/projects/Dialog/DeleteVariableDialog'; +import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog'; +import { CancelDeploymentDialog } from 'components/projects/Dialog/CancelDeploymentDialog'; +import { + Deployment, + DeploymentStatus, + Domain, + DomainStatus, + Environment, +} from 'gql-client'; +import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog'; + +const deployment: Deployment = { + id: '1', + domain: { + id: 'domain1', + branch: 'main', + name: 'example.com', + status: DomainStatus.Live, + redirectTo: null, + createdAt: '1677609600', // 2023-02-25T12:00:00Z + updatedAt: '1677613200', // 2023-02-25T13:00:00Z + }, + branch: 'main', + commitHash: 'a1b2c3d', + commitMessage: + 'lkajsdlakjsdlaijwlkjadlksjdlaisjdlakjswdalijsdlaksdj lakjsdlasjdlaijwdel akjsdlaj sldkjaliwjdeal ksjdla ijsdlaksjd', + url: 'https://deploy1.example.com', + environment: Environment.Production, + isCurrent: true, + status: DeploymentStatus.Ready, + createdBy: { + id: 'user1', + name: 'Alice', + email: 'alice@example.com', + isVerified: true, + createdAt: '1672656000', // 2023-01-01T10:00:00Z + updatedAt: '1672659600', // 2023-01-01T11:00:00Z + gitHubToken: null, + }, + createdAt: '1677676800', // 2023-03-01T12:00:00Z + updatedAt: '1677680400', // 2023-03-01T13:00:00Z +}; + +const domains: Domain[] = [ + { + id: '1', + branch: 'main', + name: 'saugat.com', + status: DomainStatus.Live, + redirectTo: null, + createdAt: '1677676800', // 2023-03-01T12:00:00Z + updatedAt: '1677680400', // 2023-03-01T13:00:00Z + }, + { + id: '2', + branch: 'main', + name: 'www.saugat.com', + status: DomainStatus.Live, + redirectTo: null, + createdAt: '1677676800', // 2023-03-01T12:00:00Z + updatedAt: '1677680400', // 2023-03-01T13:00:00Z + }, +]; + +const ModalsPage: React.FC = () => { + const [openTransferDialog, setOpenTransferDialog] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [disconnectRepoDialogOpen, setDisconnectRepoDialogOpen] = + useState(false); + const [removeMemberDialogOpen, setRemoveMemberDialogOpen] = useState(false); + const [deleteVariableDialogOpen, setDeleteVariableDialogOpen] = + useState(false); + const [deleteDomainDialogOpen, setDeleteDomainDialogOpen] = useState(false); + const [cancelDeploymentDialogOpen, setCancelDeploymentDialogOpen] = + useState(false); + const [changeProductionDialogOpen, setChangeProductionDialogOpen] = + useState(false); + const [redeployToProduction, setRedeployToProduction] = useState(false); + const [rollbackDeployment, setRollbackDeployment] = useState(false); + + return ( +
+
+

Manual Storybook

+

+ Get started by editing{' '} + + packages/frontend/src/pages/components/index.tsx + +

+ +
+ + {/* Modal */} +
+
+

Modal

+
+ {/* Modal example */} + + + + + + Modal title + +

Modal content

+
+ + + +
+
+ {/* Transfer project */} + + setOpenTransferDialog(!openTransferDialog)} + open={openTransferDialog} + handleConfirm={() => setOpenTransferDialog(!openTransferDialog)} + projectName="nextjs-boilerplate" + from="ayungavis" + to="Airfoil" + /> + {/* Delete webhook */} + + setDeleteDialogOpen((preVal) => !preVal)} + open={deleteDialogOpen} + handleConfirm={() => setDeleteDialogOpen((preVal) => !preVal)} + webhookUrl="examplehook.com" + /> + {/* Disconnect repository */} + + + setDisconnectRepoDialogOpen((preVal) => !preVal) + } + open={disconnectRepoDialogOpen} + handleConfirm={() => { + setDisconnectRepoDialogOpen((preVal) => !preVal); + }} + /> + {/* Remove member */} + + + setRemoveMemberDialogOpen((preVal) => !preVal) + } + open={removeMemberDialogOpen} + confirmButtonTitle="Yes, Remove member" + handleConfirm={() => + setRemoveMemberDialogOpen((preVal) => !preVal) + } + memberName="John Doe" + ethAddress="0x1234567890" + emailDomain="example.com" + /> + {/* Delete variable */} + + + setDeleteVariableDialogOpen((preVal) => !preVal) + } + open={deleteVariableDialogOpen} + handleConfirm={() => + setDeleteVariableDialogOpen((preVal) => !preVal) + } + variableKey="AIUTH_TOKEN" + /> + {/* Delete domain */} + + + setDeleteDomainDialogOpen((preVal) => !preVal) + } + open={deleteDomainDialogOpen} + handleConfirm={() => + setDeleteDomainDialogOpen((preVal) => !preVal) + } + projectName="Airfoil" + domainName="airfoil.com" + /> + {/* Cancel deployment */} + + + setCancelDeploymentDialogOpen(!cancelDeploymentDialogOpen) + } + open={cancelDeploymentDialogOpen} + handleConfirm={() => + setCancelDeploymentDialogOpen(!cancelDeploymentDialogOpen) + } + /> + {/* Change to production */} + + setChangeProductionDialogOpen(false)} + open={changeProductionDialogOpen} + handleConfirm={() => setChangeProductionDialogOpen(false)} + deployment={deployment} + domains={domains} + /> + {/* Redeploy to production */} + + + setRedeployToProduction((preVal) => !preVal) + } + open={redeployToProduction} + confirmButtonTitle="Redeploy" + handleConfirm={async () => + setRedeployToProduction((preVal) => !preVal) + } + deployment={deployment} + domains={deployment.domain ? [deployment.domain] : []} + /> + {/* Rollback to this deployment */} + + setRollbackDeployment((preVal) => !preVal)} + open={rollbackDeployment} + confirmButtonTitle="Rollback" + handleConfirm={async () => + setRollbackDeployment((preVal) => !preVal) + } + deployment={deployment} + newDeployment={deployment} + domains={deployment.domain ? [deployment.domain] : []} + /> +
+
+
+
+
+ ); +}; + +export default ModalsPage; diff --git a/packages/frontend/src/pages/org-slug/projects/id/settings/General.tsx b/packages/frontend/src/pages/org-slug/projects/id/settings/General.tsx index 22d4208..8c1d0bd 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/settings/General.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/settings/General.tsx @@ -6,11 +6,11 @@ import { Organization } from 'gql-client'; import { Button, Typography, Input, Option } from '@material-tailwind/react'; -import DeleteProjectDialog from '../../../../../components/projects/project/settings/DeleteProjectDialog'; -import ConfirmDialog from '../../../../../components/shared/ConfirmDialog'; -import { useGQLClient } from '../../../../../context/GQLClientContext'; -import AsyncSelect from '../../../../../components/shared/AsyncSelect'; -import { OutletContextType } from '../../../../../types'; +import DeleteProjectDialog from 'components/projects/project/settings/DeleteProjectDialog'; +import { useGQLClient } from 'context/GQLClientContext'; +import AsyncSelect from 'components/shared/AsyncSelect'; +import { OutletContextType } from 'types'; +import { TransferProjectDialog } from 'components/projects/Dialog/TransferProjectDialog'; const CopyIcon = ({ value }: { value: string }) => { return ( @@ -230,19 +230,14 @@ const GeneralTabPanel = () => { Transfer - setOpenTransferDialog(!openTransferDialog)} + setOpenTransferDialog(!openTransferDialog)} open={openTransferDialog} - confirmButtonTitle="Yes, Confirm transfer" handleConfirm={handleTransferProject} - color="blue" - > - - Upon confirmation, your project {project.name} will be transferred - from {project.organization.name} to {selectedUserOrgName}. - - + projectName={project.name} + from={project.organization.name} + to={selectedUserOrgName} + />