-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: improve delete private probe flow (#951)
* refactor(checks-filters): simplify code - remove redundant variables - clean-up unneeded if-statements * refactor(probes): improve delete probe flow - add `ConfirmModal` that supports async confirm (including errors/catch). - add business logic to avoid error when trying to delete probe that is in use * refactor(datasource): use .fetchAPI * refactor: remove redundant code * refactor: update tests * chore: fix linting issues * chore: fix ts errors after refactor * fix(ConfirmModal): wire up button text props * fix: change requests - Add missing type in `DataSource.fetchAPI` usage - Fragment code to make it more readable - Upper case first letter in `ConfirmModal` - Refactor `DeleteProbeButton` * fix: change requests #2 - Remove optional chaining * fix: change requests #2 - Remove optional chaining - Remove commented code - Navigate to probe list on delete success - Make tooltip accessible * fix: change requests #3 - Remove unused styles - Add test id to `dataTestIds.ts` * fix: linting error --------- Co-authored-by: w1kman <[email protected]>
- Loading branch information
1 parent
c281b77
commit 8be4a9e
Showing
38 changed files
with
874 additions
and
447 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const GENERIC_ERROR_MESSAGE = 'Something went wrong. Unable to fulfill the requested action.'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import React, { useEffect, useState } from 'react'; | ||
import { GrafanaTheme2 } from '@grafana/data'; | ||
import { Alert, Button, ConfirmModal as GrafanaConfirmModal, ConfirmModalProps, Modal, useTheme2 } from '@grafana/ui'; | ||
import { css } from '@emotion/css'; | ||
|
||
import type { AsyncConfirmModalProps } from './ConfirmModal.types'; | ||
|
||
import { GENERIC_ERROR_MESSAGE } from './ConfirmModal.constants'; | ||
import { getErrorWithFallback, hasOnError } from './ConfirmModal.utils'; | ||
|
||
export function ConfirmModal(props: ConfirmModalProps | AsyncConfirmModalProps) { | ||
if (!('async' in props)) { | ||
return <GrafanaConfirmModal {...props} />; | ||
} | ||
|
||
return <AsyncConfirmModal {...props} />; | ||
} | ||
|
||
export function AsyncConfirmModal({ | ||
title, | ||
isOpen, | ||
description, | ||
error: _error, | ||
onDismiss, | ||
body, | ||
dismissText = 'Cancel', | ||
confirmText = 'Confirm', | ||
...props | ||
}: AsyncConfirmModalProps) { | ||
const [error, setError] = useState<AsyncConfirmModalProps['error']>(_error); | ||
const [pending, setPending] = useState<boolean>(false); | ||
const theme2 = useTheme2(); | ||
useEffect(() => { | ||
setError(_error); | ||
}, [_error]); | ||
|
||
useEffect(() => { | ||
if (!isOpen) { | ||
setError(undefined); | ||
} | ||
}, [isOpen]); | ||
|
||
const handleDismiss = () => { | ||
!pending && onDismiss?.(); | ||
}; | ||
|
||
const styles = getStyles(theme2); | ||
|
||
const disabled = pending || error !== undefined; | ||
|
||
const handleConfirm = async () => { | ||
if (pending) { | ||
return; | ||
} | ||
|
||
setPending(true); | ||
|
||
try { | ||
const response = await props.onConfirm?.(); | ||
props.onSuccess?.(response); | ||
handleDismiss(); | ||
} catch (error: any) { | ||
if (hasOnError(props)) { | ||
props.onError(error); | ||
return; | ||
} | ||
|
||
setError(getErrorWithFallback(error, title)); | ||
} finally { | ||
setPending(false); | ||
} | ||
}; | ||
|
||
return ( | ||
<Modal className={styles.modal} title={title} isOpen={isOpen} onDismiss={handleDismiss}> | ||
{!!error && ( | ||
<Alert className={styles.alert} title={error?.name ?? `${title} error`} severity="error"> | ||
<div>{error.message ?? GENERIC_ERROR_MESSAGE}</div> | ||
</Alert> | ||
)} | ||
<div className={styles.body}>{body}</div> | ||
{!!description && <div className={styles.description}>{description}</div>} | ||
<Modal.ButtonRow> | ||
<Button variant="secondary" fill="outline" onClick={handleDismiss}> | ||
{dismissText} | ||
</Button> | ||
<Button | ||
disabled={disabled} | ||
icon={pending ? 'fa fa-spinner' : undefined} | ||
variant="destructive" | ||
onClick={handleConfirm} | ||
> | ||
{confirmText} | ||
</Button> | ||
</Modal.ButtonRow> | ||
</Modal> | ||
); | ||
} | ||
|
||
const getStyles = (theme: GrafanaTheme2) => ({ | ||
// Same as (grafana/ui) ConfirmModal | ||
modal: css({ | ||
width: '100%', | ||
maxWidth: '500px', | ||
}), | ||
alert: css({ | ||
'& *:first-letter': { | ||
textTransform: 'uppercase', | ||
}, | ||
}), | ||
body: css({ | ||
fontSize: theme.typography.h5.fontSize, | ||
}), | ||
description: css({ | ||
fontSize: theme.typography.body.fontSize, | ||
}), | ||
error: css({ | ||
fontSize: theme.typography.body.fontSize, | ||
color: theme.colors.error.text, | ||
}), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { ConfirmModalProps } from '@grafana/ui'; | ||
|
||
export interface ConfirmError { | ||
name: string; | ||
message: string; | ||
} | ||
|
||
export interface AsyncConfirmModalProps extends Omit<ConfirmModalProps, 'onConfirm'> { | ||
async: boolean; | ||
error?: ConfirmError; | ||
onSuccess?: (response: unknown) => void; | ||
onError?: (error: ConfirmError) => void; | ||
onConfirm?: () => Promise<unknown>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import type { ConfirmError } from './ConfirmModal.types'; | ||
|
||
import { GENERIC_ERROR_MESSAGE } from './ConfirmModal.constants'; | ||
|
||
export function isErrorLike(error: any): error is ConfirmError { | ||
return ( | ||
error && | ||
typeof error === 'object' && | ||
'name' in error && | ||
typeof error.name === 'string' && | ||
'message' in error && | ||
typeof error.message === 'string' | ||
); | ||
} | ||
|
||
export function getFallbackError(title: string): ConfirmError { | ||
return { name: `${title} error`, message: GENERIC_ERROR_MESSAGE }; | ||
} | ||
|
||
export function getErrorWithFallback(error: any, title: string): ConfirmError { | ||
return isErrorLike(error) ? error : getFallbackError(title); | ||
} | ||
|
||
export function hasOnError(props: { | ||
onError?: (error: ConfirmError) => void; | ||
}): props is { onError: (error: ConfirmError) => void } { | ||
return 'onError' in props && typeof props.onError === 'function'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export * from './ConfirmModal'; | ||
export type * from './ConfirmModal.types'; | ||
export * from './ConfirmModal.constants'; | ||
export * from './ConfirmModal.utils'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import React, { useCallback, useState } from 'react'; | ||
import { createPortal } from 'react-dom'; | ||
import { Button, Tooltip } from '@grafana/ui'; | ||
import { css } from '@emotion/css'; | ||
|
||
import type { BackendError, DeleteProbeButtonProps } from './DeleteProbeButton.types'; | ||
import { useDeleteProbe } from 'data/useProbes'; | ||
import { useCanEditProbe } from 'hooks/useCanEditProbe'; | ||
import { ConfirmModal } from 'components/ConfirmModal'; | ||
import { ProbeUsageLink } from 'components/ProbeUsageLink'; | ||
|
||
import { getPrettyError } from './DeleteProbeButton.utils'; | ||
|
||
export function DeleteProbeButton({ probe, onDeleteSuccess: _onDeleteSuccess }: DeleteProbeButtonProps) { | ||
const [showDeleteModal, setShowDeleteModal] = useState(false); | ||
const onDeleteSuccess = useCallback(() => { | ||
setShowDeleteModal(false); | ||
_onDeleteSuccess?.(); | ||
}, [_onDeleteSuccess]); | ||
|
||
const { mutateAsync: deleteProbe, isPending } = useDeleteProbe({ onSuccess: onDeleteSuccess }); | ||
const canEdit = useCanEditProbe(probe); | ||
const canDelete = canEdit && !probe.checks.length; | ||
const styles = getStyles(); | ||
const [error, setError] = useState<undefined | { name: string; message: string }>(); | ||
|
||
const handleError = (error: Error | BackendError) => { | ||
if (!error) { | ||
setError(undefined); | ||
} | ||
|
||
setError(getPrettyError(error, probe)); | ||
}; | ||
|
||
const handleOnClick = () => { | ||
setShowDeleteModal(true); | ||
}; | ||
|
||
if (!canDelete) { | ||
const tooltipContent = canEdit ? ( | ||
<> | ||
Unable to delete the probe because it is currently in use. | ||
<br /> | ||
<ProbeUsageLink variant="bodySmall" probe={probe} />. | ||
</> | ||
) : ( | ||
<> | ||
You do not have sufficient permissions | ||
<br /> | ||
to delete the probe <span className={styles.probeName}>'{probe.name}'</span>. | ||
</> | ||
); | ||
|
||
// Both tooltip component and button prob is used for accessibility reasons | ||
return ( | ||
<Tooltip content={tooltipContent} interactive={canEdit && !canDelete}> | ||
<Button type="button" variant="destructive" tooltip={tooltipContent} disabled> | ||
Delete probe | ||
</Button> | ||
</Tooltip> | ||
); | ||
} | ||
|
||
return ( | ||
<> | ||
<Button | ||
icon={isPending ? 'fa fa-spinner' : undefined} | ||
type="button" | ||
variant="destructive" | ||
onClick={handleOnClick} | ||
disabled={isPending} | ||
> | ||
Delete probe | ||
</Button> | ||
{createPortal( | ||
<ConfirmModal | ||
async | ||
isOpen={showDeleteModal} | ||
title="Delete probe" | ||
body={ | ||
<> | ||
Are you sure you want to delete this (<strong>{probe.name}</strong>) probe? | ||
</> | ||
} | ||
description="This action cannot be undone." | ||
confirmText="Delete probe" | ||
onConfirm={() => deleteProbe(probe)} | ||
onDismiss={() => setShowDeleteModal(false)} | ||
error={error} | ||
onError={handleError} | ||
/>, | ||
document.body | ||
)} | ||
</> | ||
); | ||
} | ||
|
||
const getStyles = () => ({ | ||
probeName: css({ | ||
display: 'inline-block', | ||
}), | ||
}); |
10 changes: 10 additions & 0 deletions
10
src/components/DeleteProbeButton/DeleteProbeButton.types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { FetchResponse } from '@grafana/runtime'; | ||
|
||
import type { ExtendedProbe } from 'types'; | ||
|
||
export type BackendError = FetchResponse<{ err: string; msg: string }>; | ||
|
||
export interface DeleteProbeButtonProps { | ||
probe: ExtendedProbe; | ||
onDeleteSuccess?: () => void; | ||
} |
14 changes: 14 additions & 0 deletions
14
src/components/DeleteProbeButton/DeleteProbeButton.utils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import type { BackendError } from './DeleteProbeButton.types'; | ||
import { Probe } from 'types'; | ||
|
||
export function getPrettyError(error: Error | BackendError, probe: Probe) { | ||
if (!error) { | ||
return undefined; | ||
} | ||
|
||
if ('data' in error && 'err' in error.data && 'msg' in error.data && typeof error.data.msg === 'string') { | ||
return { name: error.data.err, message: error.data.msg.replace(String(probe.id), `'${probe.name}'`) }; | ||
} | ||
|
||
return { name: 'Unknown error', message: 'An unknown error occurred' }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './DeleteProbeButton'; |
Oops, something went wrong.