Skip to content

Commit

Permalink
feat: alert history dropdown functionality (#5833)
Browse files Browse the repository at this point in the history
* feat: alert history dropdown actions

* chore: use query keys from react query key constant

* fix: properly handle error states for alert rule APIs

* fix: handle dropdown state using onOpenChange to fix clicking delete not closing the dropdown
  • Loading branch information
ahmadshaheer committed Sep 4, 2024
1 parent b20f906 commit 2e9cca5
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 116 deletions.
24 changes: 9 additions & 15 deletions frontend/src/api/alerts/create.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/create';

const create = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/rules', {
...props.data,
});
const response = await axios.post('/rules', {
...props.data,
});

return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};

export default create;
20 changes: 7 additions & 13 deletions frontend/src/api/alerts/delete.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/delete';

const deleteAlerts = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.delete(`/rules/${props.id}`);
const response = await axios.delete(`/rules/${props.id}`);

return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data.rules,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data.rules,
};
};

export default deleteAlerts;
25 changes: 7 additions & 18 deletions frontend/src/api/alerts/get.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/get';

const get = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(`/rules/${props.id}`);

return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
if (window.location.href.includes('alerts/history')) {
throw error as AxiosError;
}
return ErrorResponseHandler(error as AxiosError);
}
const response = await axios.get(`/rules/${props.id}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};

export default get;
28 changes: 9 additions & 19 deletions frontend/src/api/alerts/patch.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,20 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/patch';

const patch = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.patch(`/rules/${props.id}`, {
...props.data,
});
const response = await axios.patch(`/rules/${props.id}`, {
...props.data,
});

return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
if (window.location.href.includes('alerts/history')) {
throw error as AxiosError;
}

return ErrorResponseHandler(error as AxiosError);
}
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};

export default patch;
24 changes: 9 additions & 15 deletions frontend/src/api/alerts/put.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/save';

const put = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.put(`/rules/${props.id}`, {
...props.data,
});
const response = await axios.put(`/rules/${props.id}`, {
...props.data,
});

return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};

export default put;
4 changes: 4 additions & 0 deletions frontend/src/constants/reactQueryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ export const REACT_QUERY_KEY = {
ALERT_RULE_TIMELINE_TABLE: 'ALERT_RULE_TIMELINE_TABLE',
ALERT_RULE_TIMELINE_GRAPH: 'ALERT_RULE_TIMELINE_GRAPH',
GET_CONSUMER_LAG_DETAILS: 'GET_CONSUMER_LAG_DETAILS',
TOGGLE_ALERT_STATE: 'TOGGLE_ALERT_STATE',
GET_ALL_ALLERTS: 'GET_ALL_ALLERTS',
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
};
13 changes: 10 additions & 3 deletions frontend/src/pages/AlertDetails/AlertDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,19 @@ function AlertDetails(): JSX.Element {
const { routes } = useRouteTabUtils();

const {
data: { isLoading, data, isRefetching, isError },
isLoading,
isRefetching,
isError,
ruleId,
isValidRuleId,
alertDetailsResponse,
} = useGetAlertRuleDetails();

if (isError || !isValidRuleId) {
if (
isError ||
!isValidRuleId ||
(alertDetailsResponse && alertDetailsResponse.statusCode !== 200)
) {
return <NotFound />;
}

Expand All @@ -98,7 +105,7 @@ function AlertDetails(): JSX.Element {
<Divider className="divider breadcrumb-divider" />

<AlertDetailsStatusRenderer
{...{ isLoading, isError, isRefetching, data }}
{...{ isLoading, isError, isRefetching, data: alertDetailsResponse }}
/>
<Divider className="divider" />
<div className="tabs-and-filters">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,33 @@ import './ActionButtons.styles.scss';

import { Color } from '@signozhq/design-tokens';
import { Button, Divider, Dropdown, MenuProps, Switch, Tooltip } from 'antd';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { Copy, Ellipsis, PenLine, Trash2 } from 'lucide-react';
import { useAlertRuleStatusToggle } from 'pages/AlertDetails/hooks';
import {
useAlertRuleDelete,
useAlertRuleDuplicate,
useAlertRuleStatusToggle,
} from 'pages/AlertDetails/hooks';
import CopyToClipboard from 'periscope/components/CopyToClipboard';
import React from 'react';
import React, { useCallback, useState } from 'react';
import { AlertDef } from 'types/api/alerts/def';

const menu: MenuProps['items'] = [
{
key: 'rename-rule',
label: 'Rename',
icon: <PenLine size={16} color={Color.BG_VANILLA_400} />,
onClick: (): void => {},
},
{
key: 'duplicate-rule',
label: 'Duplicate',
icon: <Copy size={16} color={Color.BG_VANILLA_400} />,
onClick: (): void => {},
},
];
import { AlertHeaderProps } from '../AlertHeader';

const menuStyle: React.CSSProperties = {
padding: 0,
boxShadow: 'none',
fontSize: 14,
};

function DropdownMenuRenderer(menu: React.ReactNode): React.ReactNode {
function DropdownMenuRenderer(
menu: React.ReactNode,
handleDelete: () => void,
): React.ReactNode {
return (
<div className="dropdown-menu">
{React.cloneElement(menu as React.ReactElement, {
Expand All @@ -40,6 +39,7 @@ function DropdownMenuRenderer(menu: React.ReactNode): React.ReactNode {
type="default"
icon={<Trash2 size={16} color={Color.BG_CHERRY_400} />}
className="delete-button"
onClick={handleDelete}
>
Delete
</Button>
Expand All @@ -50,15 +50,55 @@ function DropdownMenuRenderer(menu: React.ReactNode): React.ReactNode {
function AlertActionButtons({
ruleId,
state,
alertDetails,
}: {
ruleId: string;
state: string;
alertDetails: AlertHeaderProps['alertDetails'];
}): JSX.Element {
const [dropdownOpen, setDropdownOpen] = useState(false);

const {
handleAlertStateToggle,
isAlertRuleEnabled,
} = useAlertRuleStatusToggle({ ruleId, state });

const { handleAlertDuplicate } = useAlertRuleDuplicate({
alertDetails: (alertDetails as unknown) as AlertDef,
});
const { handleAlertDelete } = useAlertRuleDelete({ ruleId: Number(ruleId) });

const handleDeleteWithClose = useCallback(() => {
handleAlertDelete();
setDropdownOpen(false);
}, [handleAlertDelete]);

const params = useUrlQuery();

const handleRename = React.useCallback(() => {
params.set(QueryParams.ruleId, String(ruleId));
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
}, [params, ruleId]);

const menu: MenuProps['items'] = React.useMemo(
() => [
{
key: 'rename-rule',
label: 'Rename',
icon: <PenLine size={16} color={Color.BG_VANILLA_400} />,
onClick: (): void => handleRename(),
},
{
key: 'duplicate-rule',
label: 'Duplicate',
icon: <Copy size={16} color={Color.BG_VANILLA_400} />,
onClick: (): void => handleAlertDuplicate(),
},
],
[handleAlertDuplicate, handleRename],
);
const isDarkMode = useIsDarkMode();

return (
<div className="alert-action-buttons">
<Switch
Expand All @@ -72,8 +112,12 @@ function AlertActionButtons({

<Dropdown
trigger={['click']}
open={dropdownOpen}
menu={{ items: menu }}
dropdownRender={DropdownMenuRenderer}
onOpenChange={setDropdownOpen}
dropdownRender={(menu: React.ReactNode): React.ReactNode =>
DropdownMenuRenderer(menu, handleDeleteWithClose)
}
>
<Tooltip title="More options">
<Ellipsis
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/pages/AlertDetails/AlertHeader/AlertHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import AlertLabels from './AlertLabels/AlertLabels';
import AlertSeverity from './AlertSeverity/AlertSeverity';
import AlertState from './AlertState/AlertState';

type AlertHeaderProps = {
export type AlertHeaderProps = {
alertDetails: {
state: string;
alert: string;
Expand Down Expand Up @@ -38,7 +38,11 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
</div>
</div>
<div className="alert-info__action-buttons">
<AlertActionButtons ruleId={alertDetails.id} state={state} />
<AlertActionButtons
alertDetails={alertDetails}
ruleId={alertDetails.id}
state={state}
/>
</div>
</div>
);
Expand Down
Loading

0 comments on commit 2e9cca5

Please sign in to comment.