Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: improve delete private probe flow #951

Merged
merged 15 commits into from
Oct 15, 2024
Merged
31 changes: 13 additions & 18 deletions src/components/CheckList/checkFilters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ const matchesFilterType = (check: Check, typeFilter: CheckTypeFilter) => {
if (typeFilter === 'all') {
return true;
}

const checkType = getCheckType(check.settings);
if (checkType === typeFilter) {
return true;
}
return false;

return checkType === typeFilter;
};

const matchesSearchFilter = ({ target, job, labels }: Check, searchFilter: string) => {
Expand All @@ -36,37 +35,33 @@ const matchesLabelFilter = ({ labels }: Check, labelFilters: string[]) => {
if (!labelFilters || labelFilters.length === 0) {
return true;
}
const result = labels?.some(({ name, value }) => {
const filtersResult = labelFilters.some((filter) => {
return filter === `${name}: ${value}`;
});
return filtersResult;

return labels?.some(({ name, value }) => {
return labelFilters.some((filter) => filter === `${name}: ${value}`);
});
return result;
};

const matchesStatusFilter = ({ enabled }: Check, { value }: SelectableValue) => {
if (
return (
value === CheckEnabledStatus.All ||
(value === CheckEnabledStatus.Enabled && enabled) ||
(value === CheckEnabledStatus.Disabled && !enabled)
) {
return true;
}
return false;
);
};

const matchesSelectedProbes = (check: Check, selectedProbes: SelectableValue[]) => {
if (selectedProbes.length === 0) {
return true;
} else {
const probeIds = selectedProbes.map((p) => p.value);
return check.probes.some((id) => probeIds.includes(id));
}

const probeIds = selectedProbes.map((p) => p.value);

return check.probes.some((id) => probeIds.includes(id));
};

export const matchesAllFilters = (check: Check, checkFilters: CheckFiltersType) => {
const { type, search, labels, status, probes } = checkFilters;

return (
Boolean(check.id) &&
matchesFilterType(check, type) &&
Expand Down
135 changes: 135 additions & 0 deletions src/components/ConfirmModal/ConfirmModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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';

interface ConfirmError {
name: string;
message: string;
}

interface AsyncConfirmModalProps extends Omit<ConfirmModalProps, 'onConfirm'> {
async: boolean;
error?: ConfirmError;
onSuccess?: (response: unknown) => void;
onError?: (error: ConfirmError) => void;
onConfirm?: () => Promise<unknown>;
}

export function ConfirmModal(props: ConfirmModalProps | AsyncConfirmModalProps) {
if (!('async' in props)) {
return <GrafanaConfirmModal {...props} />;
}

return <AsyncConfirmModal {...props} />;
}

const GENERIC_ERROR_MESSAGE = 'Something went wrong. Unable to fulfill the requested action.';

export function AsyncConfirmModal({
title,
isOpen,
description,
error: _error,
onDismiss,
body,
...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 = () => {
if (pending) {
return;
}
setPending(true);
props
.onConfirm?.()
.then((response: unknown) => {
props.onSuccess?.(response);
handleDismiss();
})
.catch((error: ConfirmError) => {
if ('onError' in props && typeof props.onError === 'function') {
props.onError(error);
return;
}

if (
error &&
'message' in error &&
typeof error.message === 'string' &&
'name' in error &&
typeof error.name === 'string'
) {
setError(error);
} else {
setError({ name: `${title} error`, message: GENERIC_ERROR_MESSAGE });
}
})
.finally(() => {
setPending(false);
});
};
w1kman marked this conversation as resolved.
Show resolved Hide resolved

return (
<Modal className={styles.modal} title={title} isOpen={isOpen} onDismiss={handleDismiss}>
{!!error && (
<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}>
Cancel
</Button>
<Button
disabled={disabled}
icon={pending ? 'fa fa-spinner' : undefined}
variant="destructive"
onClick={handleConfirm}
>
Confirm
</Button>
</Modal.ButtonRow>
</Modal>
);
}

const getStyles = (theme: GrafanaTheme2) => ({
// Same as (grafana/ui) ConfirmModal
modal: css({
width: '100%',
maxWidth: '500px',
}),
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,
}),
});
1 change: 1 addition & 0 deletions src/components/ConfirmModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ConfirmModal';
110 changes: 110 additions & 0 deletions src/components/DeleteProbeButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React, { useCallback, useState } from 'react';
import { createPortal } from 'react-dom';
import { FetchResponse } from '@grafana/runtime';
import { Button, Tooltip } from '@grafana/ui';
import { css } from '@emotion/css';

import { type ExtendedProbe } from 'types';

import { useDeleteProbe } from '../data/useProbes';
import { useCanEditProbe } from '../hooks/useCanEditProbe';
import { ConfirmModal } from './ConfirmModal';
import { ProbeUsageLink } from './ProbeUsageLink';

type BackendError = FetchResponse<{ err: string; msg: string }>;

interface DeleteProbeButtonProps {
probe: ExtendedProbe;
}

export function DeleteProbeButton({ probe }: DeleteProbeButtonProps) {
const [showDeleteModal, setShowDeleteModal] = useState(false);
const onDeleteSuccess = useCallback(() => {
setShowDeleteModal(false);
}, []);

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);
}

if ('data' in error && 'err' in error.data && 'msg' in error.data) {
setError({ name: error.data.err, message: error.data.msg });
} else {
setError({ name: 'Unknown error', message: 'An unknown error occurred' });
}
};

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}>&apos;{probe.name}&apos;</span>.
</>
);

return (
<Tooltip content={tooltipContent} interactive={canEdit && !canDelete}>
<Button type="button" variant="destructive" 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',
}),
});
Loading
Loading