diff --git a/packages/manager/.changeset/pr-9175-changed-1685739474444.md b/packages/manager/.changeset/pr-9175-changed-1685739474444.md new file mode 100644 index 00000000000..f49d6d361b9 --- /dev/null +++ b/packages/manager/.changeset/pr-9175-changed-1685739474444.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Refactor components to use TypeToConfirmDialog ([#9175](https://github.com/linode/manager/pull/9175)) diff --git a/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx b/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx index 880d95a525f..2aaea03adce 100644 --- a/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx +++ b/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx @@ -23,6 +23,10 @@ const useStyles = makeStyles()((theme: Theme) => ({ marginBottom: 0, }, }, + dialogContent: { + display: 'flex', + flexDirection: 'column', + }, })); export interface ConfirmationDialogProps extends DialogProps { @@ -53,7 +57,7 @@ export const ConfirmationDialog = (props: ConfirmationDialogProps) => { data-testid="drawer" > - + {children} {error && ( diff --git a/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.stories.tsx b/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.stories.tsx new file mode 100644 index 00000000000..f8764da9ba9 --- /dev/null +++ b/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.stories.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { TypeToConfirmDialog } from './TypeToConfirmDialog'; + +const meta: Meta = { + title: 'Components/TypeToConfirmDialog', + component: TypeToConfirmDialog, + argTypes: { + children: { + description: 'The items of the Dialog, passed-in as sub-components.', + }, + error: { description: 'Error that will be shown in the dialog.' }, + onClose: { + action: 'onClose', + description: 'Callback fired when the component requests to be closed.', + }, + onClick: { + description: 'Callback fired when the action is confirmed.', + }, + open: { description: 'Is the modal open?' }, + title: { description: 'Title that appears in the heading of the dialog.' }, + }, + args: { + open: true, + title: 'Delete Linode?', + onClose: action('onClose'), + onClick: action('onDelete'), + loading: false, + label: 'Linode Label', + children: '', + error: undefined, + entity: { + type: 'Linode', + action: 'deletion', + name: 'test linode', + primaryBtnText: 'Delete', + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ( + {args.children} + ), +}; diff --git a/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.test.tsx b/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.test.tsx index 22586a7f23e..c221d57f379 100644 --- a/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.test.tsx +++ b/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.test.tsx @@ -14,8 +14,14 @@ describe('TypeToConfirmDialog Component', () => { const { getByText } = renderWithTheme( diff --git a/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx b/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx index 06a1f3225c2..a62397ace9b 100644 --- a/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx +++ b/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx @@ -13,16 +13,26 @@ import { import { usePreferences } from 'src/queries/preferences'; interface EntityInfo { - type: 'Linode' | 'Volume' | 'NodeBalancer' | 'Bucket'; - label: string | undefined; + type: + | 'Linode' + | 'Volume' + | 'NodeBalancer' + | 'Bucket' + | 'Database' + | 'Kubernetes' + | 'AccountSetting'; + subType?: 'Cluster' | 'ObjectStorage' | 'CloseAccount'; + action?: 'deletion' | 'detachment' | 'restoration' | 'cancellation'; + name?: string | undefined; + primaryBtnText: string; } interface TypeToConfirmDialogProps { entity: EntityInfo; children: React.ReactNode; loading: boolean; - confirmationText?: string | JSX.Element; errors?: APIError[] | undefined | null; + label: string; onClick: () => void; } @@ -38,17 +48,18 @@ export const TypeToConfirmDialog = (props: CombinedProps) => { onClick, loading, entity, + label, children, - confirmationText, errors, typographyStyle, + textFieldStyle, } = props; const [confirmText, setConfirmText] = React.useState(''); const { data: preferences } = usePreferences(); const disabled = - preferences?.type_to_confirm !== false && confirmText !== entity.label; + preferences?.type_to_confirm !== false && confirmText !== entity.name; React.useEffect(() => { if (open) { @@ -56,9 +67,19 @@ export const TypeToConfirmDialog = (props: CombinedProps) => { } }, [open]); + const typeInstructions = + entity.action === 'cancellation' + ? `type your Username ` + : `type the name of the ${entity.type} ${entity.subType || ''} `; + const actions = ( - ); @@ -85,25 +105,28 @@ export const TypeToConfirmDialog = (props: CombinedProps) => { > {children} - To confirm deletion, type the name of the {entity.type} ( - {entity.label}) in the field below: + To confirm {entity.action}, {typeInstructions}( + {entity.name}) in the field below: ) } value={confirmText} typographyStyle={typographyStyle} + textFieldStyle={textFieldStyle} data-testid={'dialog-confirm-text-input'} expand onChange={(input) => { setConfirmText(input); }} visible={preferences?.type_to_confirm} + placeholder={entity.subType === 'CloseAccount' ? 'Username' : ''} /> ); diff --git a/packages/manager/src/features/Account/CloseAccountDialog.tsx b/packages/manager/src/features/Account/CloseAccountDialog.tsx index a88a0210ba4..04aaa6a24fe 100644 --- a/packages/manager/src/features/Account/CloseAccountDialog.tsx +++ b/packages/manager/src/features/Account/CloseAccountDialog.tsx @@ -2,14 +2,11 @@ import { cancelAccount } from '@linode/api-v4/lib/account'; import { APIError } from '@linode/api-v4/lib/types'; import * as React from 'react'; import { useHistory } from 'react-router-dom'; -import ActionsPanel from 'src/components/ActionsPanel'; -import { Button } from 'src/components/Button/Button'; -import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; +import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; import { makeStyles } from 'tss-react/mui'; -import { Theme } from '@mui/material/styles'; +import { Theme, styled } from '@mui/material/styles'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; -import { TypeToConfirm } from 'src/components/TypeToConfirm/TypeToConfirm'; import { TextField } from 'src/components/TextField'; import { useProfile } from 'src/queries/profile'; @@ -21,6 +18,7 @@ interface Props { const useStyles = makeStyles()((theme: Theme) => ({ dontgo: { marginTop: theme.spacing(2), + order: 1, }, })); @@ -32,7 +30,6 @@ const CloseAccountDialog = ({ closeDialog, open }: Props) => { const [comments, setComments] = React.useState(''); const [inputtedUsername, setUsername] = React.useState(''); const [canSubmit, setCanSubmit] = React.useState(false); - const { classes } = useStyles(); const history = useHistory(); const { data: profile } = useProfile(); @@ -98,86 +95,64 @@ const CloseAccountDialog = ({ closeDialog, open }: Props) => { } return ( - - } + onClick={handleCancelAccount} + loading={isClosingAccount} + inputRef={inputRef} + disabled={!canSubmit} + textFieldStyle={{ maxWidth: '415px' }} > - - - Warning: Please note this is an extremely destructive - action. Closing your account means that all services including - Linodes, Volumes, DNS Records, etc will be lost and may not be able to - be restored. - - - setUsername(input)} - inputRef={inputRef} - aria-label="username field" - value={inputtedUsername} - visible - hideInstructions - placeholder="Username" - /> + {errors ? : null} + + + + Warning: Please note this is an extremely + destructive action. Closing your account means that all services + Linodes, Volumes, DNS Records, etc will be lost and may not be able + be restored. + + + We’d hate to see you go. Please let us know what we could be doing better in the comments section below. After your account is closed, you’ll be directed to a quick survey so we can better gauge your feedback. - setComments(e.target.value)} - optional - placeholder="Provide Feedback" - rows={1} - value={comments} - aria-label="Optional comments field" - /> - + + setComments(e.target.value)} + optional + placeholder="Provide Feedback" + rows={1} + value={comments} + aria-label="Optional comments field" + /> + + ); }; -interface ActionsProps { - onClose: () => void; - onSubmit: () => void; - isCanceling: boolean; - disabled: boolean; -} +// The order property helps inject the TypeToConfirm input field in the TypeToConfirmDialog when the components +// below are passed in as the children prop. +const StyledNoticeWrapper = styled('div')(() => ({ + order: 0, +})); -const Actions = ({ - disabled, - isCanceling, - onClose, - onSubmit, -}: ActionsProps) => { - return ( - - - - - ); -}; +const StyledCommentSectionWrapper = styled('div')(() => ({ + order: 2, +})); export default React.memo(CloseAccountDialog); diff --git a/packages/manager/src/features/Account/EnableObjectStorage.tsx b/packages/manager/src/features/Account/EnableObjectStorage.tsx index f54136a074a..c71cb335369 100644 --- a/packages/manager/src/features/Account/EnableObjectStorage.tsx +++ b/packages/manager/src/features/Account/EnableObjectStorage.tsx @@ -3,21 +3,17 @@ import { cancelObjectStorage } from '@linode/api-v4/lib/object-storage'; import { APIError } from '@linode/api-v4/lib/types'; import * as React from 'react'; import { Link } from 'react-router-dom'; +import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; import { Accordion } from 'src/components/Accordion'; -import ActionsPanel from 'src/components/ActionsPanel'; import { Button } from 'src/components/Button/Button'; -import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { Notice } from 'src/components/Notice/Notice'; -import { TypeToConfirm } from 'src/components/TypeToConfirm/TypeToConfirm'; import { Typography } from 'src/components/Typography'; import ExternalLink from 'src/components/ExternalLink'; import Grid from '@mui/material/Unstable_Grid2'; import { updateAccountSettingsData } from 'src/queries/accountSettings'; -import { usePreferences } from 'src/queries/preferences'; import { useProfile } from 'src/queries/profile'; import { queryKey } from 'src/queries/objectStorage'; import { useQueryClient } from 'react-query'; - interface Props { object_storage: AccountSettings['object_storage']; } @@ -73,13 +69,9 @@ export const EnableObjectStorage = (props: Props) => { const [isOpen, setOpen] = React.useState(false); const [error, setError] = React.useState(); const [isLoading, setLoading] = React.useState(false); - const [confirmText, setConfirmText] = React.useState(''); - const { data: preferences } = usePreferences(); const { data: profile } = useProfile(); const queryClient = useQueryClient(); const username = profile?.username; - const disabledConfirm = - preferences?.type_to_confirm !== false && confirmText !== username; const handleClose = () => { setOpen(false); @@ -104,28 +96,6 @@ export const EnableObjectStorage = (props: Props) => { .catch(handleError); }; - const actions = ( - - - - - - ); - return ( <> @@ -134,35 +104,30 @@ export const EnableObjectStorage = (props: Props) => { openConfirmationModal={() => setOpen(true)} /> - handleClose()} - title="Cancel Object Storage" - actions={actions} + loading={isLoading} + onClose={handleClose} + onClick={handleSubmit} > + {error ? : null} - + Warning: Canceling Object Storage will permanently delete all buckets and their objects. Object Storage Access Keys will be revoked. - setConfirmText(input)} - expand - value={confirmText} - confirmationText={ - - To confirm cancellation, type your username ({username}) in - the field below: - - } - visible={preferences?.type_to_confirm} - /> - + ); }; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/RestoreFromBackupDialog.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/RestoreFromBackupDialog.tsx index 4575fdd1644..455be5a9532 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/RestoreFromBackupDialog.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseBackups/RestoreFromBackupDialog.tsx @@ -2,15 +2,11 @@ import { Database, DatabaseBackup } from '@linode/api-v4/lib/databases'; import { useSnackbar } from 'notistack'; import * as React from 'react'; import { useHistory } from 'react-router-dom'; -import ActionsPanel from 'src/components/ActionsPanel'; -import { Button } from 'src/components/Button/Button'; -import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; +import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; import { DialogProps } from 'src/components/Dialog/Dialog'; import { Notice } from 'src/components/Notice/Notice'; -import { TypeToConfirm } from 'src/components/TypeToConfirm/TypeToConfirm'; import { Typography } from 'src/components/Typography'; import { useRestoreFromBackupMutation } from 'src/queries/databases'; -import { usePreferences } from 'src/queries/preferences'; import { useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import formatDate from 'src/utilities/formatDate'; @@ -23,14 +19,9 @@ interface Props extends Omit { } export const RestoreFromBackupDialog: React.FC = (props) => { - const { database, backup, onClose, open, ...rest } = props; - + const { database, backup, onClose, open } = props; const history = useHistory(); const { enqueueSnackbar } = useSnackbar(); - - const [confirmationText, setConfirmationText] = React.useState(''); - - const { data: preferences } = usePreferences(); const { data: profile } = useProfile(); const { @@ -49,40 +40,23 @@ export const RestoreFromBackupDialog: React.FC = (props) => { }); }; - const actions = ( - - - - - ); - - React.useEffect(() => { - if (open) { - setConfirmationText(''); - } - }, [open]); - return ( - {error ? ( = (props) => { existing data on this cluster. - - To confirm restoration, type the name of the database cluster ( - {database.label}) in the field below. - - } - onChange={(input) => setConfirmationText(input)} - value={confirmationText} - label="Database Label" - visible={preferences?.type_to_confirm} - placeholder={database.label} - /> - + ); }; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsDeleteClusterDialog.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsDeleteClusterDialog.tsx index 5367d1f4b21..ade1e5b0ed2 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsDeleteClusterDialog.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsDeleteClusterDialog.tsx @@ -2,15 +2,11 @@ import { Engine } from '@linode/api-v4/lib/databases'; import { useSnackbar } from 'notistack'; import * as React from 'react'; import { useHistory } from 'react-router-dom'; -import ActionsPanel from 'src/components/ActionsPanel'; -import { Button } from 'src/components/Button/Button'; -import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { Typography } from 'src/components/Typography'; import { Notice } from 'src/components/Notice/Notice'; -import { TypeToConfirm } from 'src/components/TypeToConfirm/TypeToConfirm'; +import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; import { useDeleteDatabaseMutation } from 'src/queries/databases'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import { usePreferences } from 'src/queries/preferences'; interface Props { open: boolean; @@ -20,49 +16,17 @@ interface Props { databaseLabel: string; } -const renderActions = ( - disabled: boolean, - loading: boolean, - onClose: () => void, - onDelete: () => void -) => ( - - - - -); - export const DatabaseSettingsDeleteClusterDialog: React.FC = (props) => { const { open, onClose, databaseID, databaseEngine, databaseLabel } = props; const { enqueueSnackbar } = useSnackbar(); - const { data: preferences } = usePreferences(); const { mutateAsync: deleteDatabase } = useDeleteDatabaseMutation( databaseEngine, databaseID ); const defaultError = 'There was an error deleting this Database Cluster.'; const [error, setError] = React.useState(''); - const [confirmText, setConfirmText] = React.useState(''); const [isLoading, setIsLoading] = React.useState(false); const { push } = useHistory(); - const disabled = - preferences?.type_to_confirm !== false && confirmText !== databaseLabel; const onDeleteCluster = () => { setIsLoading(true); @@ -82,13 +46,22 @@ export const DatabaseSettingsDeleteClusterDialog: React.FC = (props) => { }; return ( - + {error ? : null} Warning: Deleting your entire database will delete @@ -96,21 +69,7 @@ export const DatabaseSettingsDeleteClusterDialog: React.FC = (props) => { may result in permanent data loss. This action cannot be undone. - setConfirmText(input)} - expand - value={confirmText} - confirmationText={ - - To confirm deletion, type the name of the database cluster ( - {databaseLabel}) in the field below: - - } - visible={preferences?.type_to_confirm} - /> - + ); }; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/DeleteKubernetesClusterDialog.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/DeleteKubernetesClusterDialog.tsx index 3fb71969fa2..b234dd01d10 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/DeleteKubernetesClusterDialog.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/DeleteKubernetesClusterDialog.tsx @@ -1,11 +1,7 @@ import * as React from 'react'; -import ActionsPanel from 'src/components/ActionsPanel'; -import { Button } from 'src/components/Button/Button'; -import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; +import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; import { Typography } from 'src/components/Typography'; -import { TypeToConfirm } from 'src/components/TypeToConfirm/TypeToConfirm'; import { Notice } from 'src/components/Notice/Notice'; -import { usePreferences } from 'src/queries/preferences'; import { useDeleteKubernetesClusterMutation } from 'src/queries/kubernetes'; import { KubeNodePoolResponse } from '@linode/api-v4'; import { useHistory } from 'react-router-dom'; @@ -25,24 +21,12 @@ export const getTotalLinodes = (pools: KubeNodePoolResponse[]) => { export const DeleteKubernetesClusterDialog = (props: Props) => { const { clusterLabel, clusterId, open, onClose } = props; - const { mutateAsync: deleteCluster, isLoading: isDeleting, error, } = useDeleteKubernetesClusterMutation(); - const history = useHistory(); - const { data: preferences } = usePreferences(); - const [confirmText, setConfirmText] = React.useState(''); - const disabled = - preferences?.type_to_confirm !== false && confirmText !== clusterLabel; - - React.useEffect(() => { - if (open && confirmText !== '') { - setConfirmText(''); - } - }, [open]); const onDelete = () => { deleteCluster({ id: clusterId }).then(() => { @@ -51,37 +35,23 @@ export const DeleteKubernetesClusterDialog = (props: Props) => { }); }; - const actions = ( - - - - - ); - return ( - + {error ? : null} Warning: @@ -94,23 +64,6 @@ export const DeleteKubernetesClusterDialog = (props: Props) => { - - To confirm deletion, type the name of the cluster ( - {clusterLabel}) in the field below: - - } - value={confirmText} - typographyStyle={{ marginTop: '10px' }} - data-testid={'dialog-confirm-text-input'} - expand - onChange={(input) => { - setConfirmText(input); - }} - visible={preferences?.type_to_confirm} - /> - + ); }; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx index 9d975ed5a0a..9a1da6e09fe 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx @@ -52,7 +52,13 @@ export const LinodeSettingsDeletePanel = ({ linodeId, isReadOnly }: Props) => { { return ( { { delete: onDelete, }[props.mode]; - const action = { - detach: { - verb: 'Detach', - noun: 'detachment', - }, - delete: { - verb: 'Delete', - noun: 'deletion', - }, - }[props.mode]; - const loading = { detach: detachLoading, delete: deleteLoading, @@ -111,17 +100,17 @@ export const DestructiveVolumeDialog = (props: Props) => { return ( - To confirm {action.noun}, type the name of the Volume ({label}) - in the field below: - - } typographyStyle={{ marginTop: '10px' }} > {error && }