Modal cleanup, begin transitioning towards the new dialog

This commit is contained in:
DaneEveritt 2022-06-20 11:17:33 -04:00
parent 3834aca3fe
commit 7dd74ecc9d
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
10 changed files with 121 additions and 96 deletions

View file

@ -36,6 +36,7 @@ return [
],
],
'server' => [
'reinstall' => 'Reinstalled server',
'backup' => [
'download' => 'Downloaded the :name backup',
'delete' => 'Deleted the :name backup',
@ -88,7 +89,6 @@ return [
],
'settings' => [
'rename' => 'Renamed the server from :old to :new',
'reinstall' => 'Triggered a server reinstall',
],
'startup' => [
'edit' => 'Changed the :variable variable from ":old" to ":new"',

View file

@ -5,46 +5,42 @@ import getApiKeys, { ApiKey } from '@/api/account/getApiKeys';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faKey, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import ConfirmationModal from '@/components/elements/ConfirmationModal';
import deleteApiKey from '@/api/account/deleteApiKey';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import FlashMessageRender from '@/components/FlashMessageRender';
import { httpErrorToHuman } from '@/api/http';
import { format } from 'date-fns';
import PageContentBlock from '@/components/elements/PageContentBlock';
import tw from 'twin.macro';
import GreyRowBox from '@/components/elements/GreyRowBox';
import { Dialog } from '@/components/elements/dialog';
import { useFlashKey } from '@/plugins/useFlash';
import Code from '@/components/elements/Code';
export default () => {
const [ deleteIdentifier, setDeleteIdentifier ] = useState('');
const [ keys, setKeys ] = useState<ApiKey[]>([]);
const [ loading, setLoading ] = useState(true);
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const { clearAndAddHttpError } = useFlashKey('account');
useEffect(() => {
clearFlashes('account');
getApiKeys()
.then(keys => setKeys(keys))
.then(() => setLoading(false))
.catch(error => {
console.error(error);
addError({ key: 'account', message: httpErrorToHuman(error) });
});
.catch(error => clearAndAddHttpError(error));
}, []);
const doDeletion = (identifier: string) => {
setLoading(true);
clearFlashes('account');
clearAndAddHttpError();
deleteApiKey(identifier)
.then(() => setKeys(s => ([
...(s || []).filter(key => key.identifier !== identifier),
])))
.catch(error => {
console.error(error);
addError({ key: 'account', message: httpErrorToHuman(error) });
})
.then(() => setLoading(false));
.catch(error => clearAndAddHttpError(error))
.then(() => {
setLoading(false);
setDeleteIdentifier('');
});
};
return (
@ -56,19 +52,15 @@ export default () => {
</ContentBox>
<ContentBox title={'API Keys'} css={tw`flex-1 overflow-hidden mt-8 md:mt-0 md:ml-8`}>
<SpinnerOverlay visible={loading}/>
<ConfirmationModal
visible={!!deleteIdentifier}
title={'Confirm key deletion'}
buttonText={'Yes, delete key'}
onConfirmed={() => {
doDeletion(deleteIdentifier);
setDeleteIdentifier('');
}}
onModalDismissed={() => setDeleteIdentifier('')}
<Dialog.Confirm
title={'Delete API Key'}
confirm={'Delete Key'}
open={!!deleteIdentifier}
onClose={() => setDeleteIdentifier('')}
onConfirmed={() => doDeletion(deleteIdentifier)}
>
Are you sure you wish to delete this API key? All requests using it will immediately be
invalidated and will fail.
</ConfirmationModal>
All requests using the <Code>{deleteIdentifier}</Code> key will be invalidated.
</Dialog.Confirm>
{
keys.length === 0 ?
<p css={tw`text-center text-sm`}>

View file

@ -55,7 +55,7 @@ export default () => {
{format(key.createdAt, 'MMM do, yyyy HH:mm')}
</p>
</div>
<DeleteSSHKeyButton fingerprint={key.fingerprint} />
<DeleteSSHKeyButton name={key.name} fingerprint={key.fingerprint} />
</GreyRowBox>
))
}

View file

@ -3,10 +3,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import React, { useState } from 'react';
import { useFlashKey } from '@/plugins/useFlash';
import ConfirmationModal from '@/components/elements/ConfirmationModal';
import { deleteSSHKey, useSSHKeys } from '@/api/account/ssh-keys';
import { Dialog } from '@/components/elements/dialog';
import Code from '@/components/elements/Code';
export default ({ fingerprint }: { fingerprint: string }) => {
export default ({ name, fingerprint }: { name: string; fingerprint: string }) => {
const { clearAndAddHttpError } = useFlashKey('account');
const [ visible, setVisible ] = useState(false);
const { mutate } = useSSHKeys();
@ -19,22 +20,22 @@ export default ({ fingerprint }: { fingerprint: string }) => {
deleteSSHKey(fingerprint),
])
.catch((error) => {
mutate(undefined, true);
mutate(undefined, true).catch(console.error);
clearAndAddHttpError(error);
});
};
return (
<>
<ConfirmationModal
visible={visible}
title={'Confirm Key Deletion'}
buttonText={'Yes, Delete SSH Key'}
<Dialog.Confirm
open={visible}
title={'Delete SSH Key'}
confirm={'Delete Key'}
onConfirmed={onClick}
onModalDismissed={() => setVisible(false)}
onClose={() => setVisible(false)}
>
Are you sure you wish to delete this SSH key?
</ConfirmationModal>
Removing the <Code>{name}</Code> SSH key will invalidate its usage across the Panel.
</Dialog.Confirm>
<button css={tw`ml-4 p-2 text-sm`} onClick={() => setVisible(true)}>
<FontAwesomeIcon
icon={faTrashAlt}

View file

@ -0,0 +1,18 @@
import React from 'react';
import classNames from 'classnames';
interface CodeProps {
dark?: boolean | undefined;
children: React.ReactChild | React.ReactFragment | React.ReactPortal;
}
export default ({ dark, children }: CodeProps) => (
<code
className={classNames('font-mono text-sm px-2 py-1 rounded', {
'bg-neutral-700': !dark,
'bg-neutral-900 text-gray-100': dark,
})}
>
{children}
</code>
);

View file

@ -87,7 +87,7 @@ export default ({ activity, children }: Props) => {
<span className={'text-gray-400'}>&nbsp;|&nbsp;</span>
<Tooltip
placement={'right'}
content={format(activity.timestamp, 'MMM do, yyyy h:mma')}
content={format(activity.timestamp, 'MMM do, yyyy H:mm:ss')}
>
<span>
{formatDistanceToNowStrict(activity.timestamp, { addSuffix: true })}

View file

@ -0,0 +1,22 @@
import React from 'react';
import { Dialog } from '@/components/elements/dialog/index';
import { DialogProps } from '@/components/elements/dialog/Dialog';
import { Button } from '@/components/elements/button/index';
type ConfirmationProps = Omit<DialogProps, 'description' | 'children'> & {
children: React.ReactNode;
confirm?: string | undefined;
onConfirmed: () => void;
}
export default ({ confirm = 'Okay', children, onConfirmed, ...props }: ConfirmationProps) => {
return (
<Dialog {...props} description={typeof children === 'string' ? children : undefined}>
{typeof children !== 'string' && children}
<Dialog.Buttons>
<Button.Text onClick={props.onClose}>Cancel</Button.Text>
<Button.Danger onClick={onConfirmed}>{confirm}</Button.Danger>
</Dialog.Buttons>
</Dialog>
);
};

View file

@ -5,13 +5,14 @@ import { XIcon } from '@heroicons/react/solid';
import DialogIcon from '@/components/elements/dialog/DialogIcon';
import { AnimatePresence, motion } from 'framer-motion';
import classNames from 'classnames';
import ConfirmationDialog from '@/components/elements/dialog/ConfirmationDialog';
interface Props {
export interface DialogProps {
open: boolean;
onClose: () => void;
hideCloseIcon?: boolean;
title?: string;
description?: string;
description?: string | undefined;
children?: React.ReactNode;
}
@ -19,7 +20,7 @@ const DialogButtons = ({ children }: { children: React.ReactNode }) => (
<>{children}</>
);
const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }: Props) => {
const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }: DialogProps) => {
const items = React.Children.toArray(children || []);
const [ buttons, icon, content ] = [
// @ts-expect-error
@ -59,9 +60,9 @@ const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }:
>
<div className={'flex p-6'}>
{icon && <div className={'mr-4'}>{icon}</div>}
<div className={'flex-1 max-h-[70vh] overflow-y-scroll overflow-x-hidden'}>
<div className={'flex-1 max-h-[70vh]'}>
{title &&
<HDialog.Title className={'font-header text-xl font-medium mb-2 text-white pr-4'}>
<HDialog.Title className={'font-header text-xl font-medium mb-2 text-gray-50 pr-4'}>
{title}
</HDialog.Title>
}
@ -91,6 +92,10 @@ const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }:
);
};
const _Dialog = Object.assign(Dialog, { Buttons: DialogButtons, Icon: DialogIcon });
const _Dialog = Object.assign(Dialog, {
Confirm: ConfirmationDialog,
Buttons: DialogButtons,
Icon: DialogIcon,
});
export default _Dialog;

View file

@ -13,7 +13,6 @@ import getBackupDownloadUrl from '@/api/server/backups/getBackupDownloadUrl';
import useFlash from '@/plugins/useFlash';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import deleteBackup from '@/api/server/backups/deleteBackup';
import ConfirmationModal from '@/components/elements/ConfirmationModal';
import Can from '@/components/elements/Can';
import tw from 'twin.macro';
import getServerBackups from '@/api/swr/getServerBackups';
@ -22,6 +21,7 @@ import { ServerContext } from '@/state/server';
import Input from '@/components/elements/Input';
import { restoreServerBackup } from '@/api/server/backups';
import http, { httpErrorToHuman } from '@/api/http';
import { Dialog } from '@/components/elements/dialog';
interface Props {
backup: ServerBackup;
@ -103,35 +103,29 @@ export default ({ backup }: Props) => {
return (
<>
<ConfirmationModal
visible={modal === 'unlock'}
title={'Unlock this backup?'}
<Dialog.Confirm
open={modal === 'unlock'}
onClose={() => setModal('')}
title={`Unlock "${backup.name}"`}
onConfirmed={onLockToggle}
onModalDismissed={() => setModal('')}
buttonText={'Yes, unlock'}
>
Are you sure you want to unlock this backup? It will no longer be protected from automated or
accidental deletions.
</ConfirmationModal>
<ConfirmationModal
visible={modal === 'restore'}
title={'Restore this backup?'}
buttonText={'Restore backup'}
This backup will no longer be protected from automated or accidental deletions.
</Dialog.Confirm>
<Dialog.Confirm
open={modal === 'restore'}
onClose={() => setModal('')}
confirm={'Restore'}
title={`Restore "${backup.name}"`}
onConfirmed={() => doRestorationAction()}
onModalDismissed={() => setModal('')}
>
<p css={tw`text-neutral-300`}>
This server will be stopped in order to restore the backup. Once the backup has started you will
not be able to control the server power state, access the file manager, or create additional backups
until it has completed.
<p>
Your server will be stopped. You will not be able to control the power state, access the file
manager, or create additional backups until completed.
</p>
<p css={tw`text-neutral-300 mt-4`}>
Are you sure you want to continue?
</p>
<p css={tw`mt-4 -mb-2 bg-neutral-900 p-3 rounded`}>
<p css={tw`mt-4 -mb-2 bg-gray-700 p-3 rounded`}>
<label
htmlFor={'restore_truncate'}
css={tw`text-base text-neutral-200 flex items-center cursor-pointer`}
css={tw`text-base flex items-center cursor-pointer`}
>
<Input
type={'checkbox'}
@ -141,27 +135,26 @@ export default ({ backup }: Props) => {
checked={truncate}
onChange={() => setTruncate(s => !s)}
/>
Remove all files and folders before restoring this backup.
Delete all files before restoring backup.
</label>
</p>
</ConfirmationModal>
<ConfirmationModal
visible={modal === 'delete'}
title={'Delete this backup?'}
buttonText={'Yes, delete backup'}
onConfirmed={() => doDeletion()}
onModalDismissed={() => setModal('')}
</Dialog.Confirm>
<Dialog.Confirm
title={`Delete "${backup.name}"`}
confirm={'Continue'}
open={modal === 'delete'}
onClose={() => setModal('')}
onConfirmed={doDeletion}
>
Are you sure you wish to delete this backup? This is a permanent operation and the backup cannot
be recovered once deleted.
</ConfirmationModal>
This is a permanent operation. The backup cannot be recovered once deleted.
</Dialog.Confirm>
<SpinnerOverlay visible={loading} fixed/>
{backup.isSuccessful ?
<DropdownMenu
renderToggle={onClick => (
<button
onClick={onClick}
css={tw`text-neutral-200 transition-colors duration-150 hover:text-neutral-100 p-2`}
css={tw`text-gray-200 transition-colors duration-150 hover:text-gray-100 p-2`}
>
<FontAwesomeIcon icon={faEllipsisH}/>
</button>
@ -203,7 +196,7 @@ export default ({ backup }: Props) => {
:
<button
onClick={() => setModal('delete')}
css={tw`text-neutral-200 transition-colors duration-150 hover:text-neutral-100 p-2`}
css={tw`text-gray-200 transition-colors duration-150 hover:text-gray-100 p-2`}
>
<FontAwesomeIcon icon={faTrashAlt}/>
</button>

View file

@ -1,23 +1,21 @@
import React, { useEffect, useState } from 'react';
import { ServerContext } from '@/state/server';
import TitledGreyBox from '@/components/elements/TitledGreyBox';
import ConfirmationModal from '@/components/elements/ConfirmationModal';
import reinstallServer from '@/api/server/reinstallServer';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import { httpErrorToHuman } from '@/api/http';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
import { Dialog } from '@/components/elements/dialog';
export default () => {
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const [ isSubmitting, setIsSubmitting ] = useState(false);
const [ modalVisible, setModalVisible ] = useState(false);
const { addFlash, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const reinstall = () => {
clearFlashes('settings');
setIsSubmitting(true);
reinstallServer(uuid)
.then(() => {
addFlash({
@ -31,10 +29,7 @@ export default () => {
addFlash({ key: 'settings', type: 'error', message: httpErrorToHuman(error) });
})
.then(() => {
setIsSubmitting(false);
setModalVisible(false);
});
.then(() => setModalVisible(false));
};
useEffect(() => {
@ -43,17 +38,16 @@ export default () => {
return (
<TitledGreyBox title={'Reinstall Server'} css={tw`relative`}>
<ConfirmationModal
<Dialog.Confirm
open={modalVisible}
title={'Confirm server reinstallation'}
buttonText={'Yes, reinstall server'}
confirm={'Yes, reinstall server'}
onClose={() => setModalVisible(false)}
onConfirmed={reinstall}
showSpinnerOverlay={isSubmitting}
visible={modalVisible}
onModalDismissed={() => setModalVisible(false)}
>
Your server will be stopped and some files may be deleted or modified during this process, are you sure
you wish to continue?
</ConfirmationModal>
</Dialog.Confirm>
<p css={tw`text-sm`}>
Reinstalling your server will stop it, and then re-run the installation script that initially
set it up.&nbsp;