Yunus M e97d0ea51c
Feat: alert history (#5774)
* feat: tabs and filters for alert history page (#5655)

* feat: alert history page route and component setup

* feat: alert history basic tabs and fitlers UI

* feat: route based tabs for alert history and overview and improve the UI to match designs

* chore: unused components and files cleanup

* chore: improve alert history and overview route paths

* chore: use parent selector in scss files

* chore: alert -> alerts

* feat: alert rule details metadata header (#5675)

* feat: alert history basic tabs and fitlers UI

* feat: route based tabs for alert history and overview and improve the UI to match designs

* chore: unused components and files cleanup

* feat: copy to clipboard component

* feat: see more component

* feat: key value label component

* feat: alert rule details meta data header

* fix: apply the missing changes

* chore: uncomment the alert status with static data

* chore: compress the alert status svg icons and define props, types, and defaultProps

* feat: alert rule history skeleton using static data (#5688)

* feat: alert history basic tabs and fitlers UI

* feat: route based tabs for alert history and overview and improve the UI to match designs

* feat: top contributors UI using static data

* feat: avg. resolution time and total triggered stats card UI using static data

* feat: tabs component

* feat: timeline tabs and filters

* feat: overall status graph UI using dummy data with graph placeholder

* feat: timeline table and pagination UI using dummy data

* fix: bugfix in reset tabs

* feat: add popover to go to logs/traces to top contributors and timeline table

* chore: remove comments

* chore: rename AlertIcon to AlertState

* fix: add cursor pointer to timeline table rows

* feat: add parent tabs to alert history

* chore: add icon to the configure tab

* fix: display popover on hovering the more button in see more component

* fix: wrap key value label

* feat: alert rule history enable/disable toggle UI

* Feat: get alert history data from API (#5718)

* feat: alert history basic tabs and fitlers UI

* feat: route based tabs for alert history and overview and improve the UI to match designs

* feat: data state renderer component

* feat: get total triggered and avg. resolution cards data from API

* fix: hide stats card if we get NaN

* chore: improve rule stats types

* feat: get top contributors data from API

* feat: get timeline table data from API

* fix: properly render change percentage indicator

* feat: total triggered and avg resolution empty states

* fix: fix stats height issue that would cause short border-right in empty case

* feat: top contributors empty state

* fix: fix table and graph borders

* feat: build alert timeline labels filter and handle client side filtering

* fix: select the first tab on clicking reset

* feat: set param and send in payload on clicking timeline filter tabs

* Feat: alert history timeline remaining subtasks except graphs (#5720)

* feat: alert history basic tabs and fitlers UI

* feat: route based tabs for alert history and overview and improve the UI to match designs

* feat: implement timeline table sorting

* chore: add initial count to see more and alert labels

* chore: move PaginationInfoText component to /periscope

* chore: implement top contributor rows using Ant Table

* feat: top contributors view all

* fix: hide border for last row and prevent layout shift in top contributors by specifying height

* feat: properly display duration in average resolution time

* fix: properly display normal alert rule state

* feat: add/remove view all top contributors param to url on opening/closing view all

* feat: calculate start and end time from relative time and add/remove param to url

* fix: fix console warnings

* fix: enable timeline table query only if start and end times exist

* feat: handle enable/disable alert rule toggle request

* chore: replace string values with constants

* fix: hide stats card if only past data is available + remove unnecessary states from AlertState

* fix: redirect configure alert rule to alert overview tab

* fix: display total triggers in timeline chart wrapper based on API response data

* fix: choosing the same relative time doesn't udpate start and end time

* Feat: total triggered and avg. resolution time graph (#5750)

* feat: alert history basic tabs and fitlers UI

* feat: route based tabs for alert history and overview and improve the UI to match designs

* feat: handle enable/disable alert rule toggle request

* feat: stats card line chart

* fix: overall improvements to stats card graph

* fix: overall UI improvements to match the Figma screens

* chore: remove duplicate hook

* fix: make the changes w.r.t timeline table API changes to prevent breaking the page

* fix: update stats card null check based on updated API response

* feat: stats card no previous data UI

* feat: redirect to 404 page if rule id is invalid

* chore: improve alert enable toggle success toast message

* feat: get top contributors row and timeline table row related logs and traces links from API

* feat: get total items from API and make pagination work

* feat: implement timeline filters based on API response

* fix: in case of current and target units, convert the value unit in timeline table

* fix: timeline table y axis unit null check

* fix: hide stats card graph if only a single entry is there in timeseries

* chore: redirect alert from all alerts to overview tab

* fix: prevent adding extra unnecessary params on clicking alerts top level tabs

* chore: use conditional alert popover in timeline table and import the scss file

* fix: prevent infinity if we receive totalPastTriggers as '0'

* fix: improve UI to be pixel perfect based on figma designs

* fix: fix the incorrect change direction

* fix: add height to top contributors row

* feat: alert history light mode

* fix: remove the extra padding from alert overview query builder tabs

* chore: overall improvements

* chore: remove mock file

* fix: overall improvements

* fix: add dark mode support for top contributors empty state

* chore: improve timeline chart placeholder bg in light mode

* Feat: alert history horizontal timeline chart (#5773)

* feat: timeline horizontal chart

* fix: remove the labels from horizontal timeline chart

* chore: add null check to timeline chart

* chore: hide cursor from timeline chart

* fix: fix the blank container being displayed in loading state

* fix: alert history UI fixes (#5776)

* fix: remove extra padding from alert overview query section tabs

* fix: add padding to alert overview container

* fix: improve breadcrumb click behavior

* chore: temporarily hide reset button from alert details timepicker

* fix: improve breadcrumb click behavior

* chore: hide alert firing since

* fix: don't use the data state renderer for timeline table

* fix: alert history pr review changes (#5778)

* chore: rename alert history scss files in pascal case

* fix: use proper variables

* chore: use color variable for action button dropdown item

* chore: improve the directory structure for alert history components

* chore: move inline style to scss file and extract dropdown renderer component

* chore: use colors from Color instead of css variables inside tsx files

* chore: return null in default case

* chore: update alert details spinner tip

* chore: timelinePlugin warnings and remove file wide warning disabling

* chore: change Arial to Geist Mono in timeline plugin

* feat: alert history remaining feats (#5825)

* fix: add switch case for inactive state to alert state component

* feat: add API enabled label search similar to Query Builder

* feat: add reset button to date and time picker

* feat: add vertical timeline chart using static data

* chore: use Colors instead of hex + dummy data for 90 days

* fix: label search light mode UI

* fix: remove placeholder logic, and display vertical charts if more than 1 day

* chore: extract dayjs manipulate types to a  constant

* fix: hide the overflow of top contributors card

* fix: throw instead of return error to prevent breaking alert history page in case of error

* chore: temporarily comment alert history vertical charts

* chore: calculate start and end times from relative time and remove query params (#5828)

* chore: calculate start and end times from relative time and remove query params

* fix: hide reset button if selected time is 30m

* feat: alert history dropdown functionality (#5833)

* 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

* Fix: bugfixes and overall improvements to alert history  (#5841)

* fix: don't display severity label

* chore: remove id from alert header

* chore: add tooltip to enable/disable alert toggle

* chore: update enable/disbale toast message

* fix: set default relative time to 6h if relative time is not provided

* chore: update empty top contributors text and remove configure alert

* chore: temporarily hide value column from timeline column

* fix: use correct links for logs and traces in alert popover

* fix: properly set timeline table offset

* fix: display all values in graph

* fix: resolve conflicts

* chore: remove style for value column in timeline table

* chore: temporarily hide labels search

* fix: incorrect current page in pagination info text

* chore: remove label QB search

* chore: remove value column

* chore: remove commented code

* fix: show traces button when trace link is available

* fix: display horizontal chart even for a single entry

* fix: show inactive state in horizontal similar to normal state

* fix: properly render inactive state in horizontal chart

* fix: properly handle preserving alert toggle between overview and history tabs

* feat: get page size from query param

* chore: remove commented code + minor refactor

* chore: remove tsconfi.tmp

* fix: don't add default relative time if start and times exist in the url

* feat: display date range preview for stat cards

* chore: remove custom dropdown renderer component

* Fix: UI feedback changes (#5852)

* fix: add divider before delete button

* fix: timeline section title color in lightmode

* fix: remove the extra border from alert history tabs

* fix: populate alert rule disabled state on toggling alert state (#5854)

---------

Co-authored-by: Shaheer Kochai <ashaheerki@gmail.com>
2024-09-04 20:26:10 +04:30

526 lines
14 KiB
TypeScript

import { FilterValue, SorterResult } from 'antd/es/table/interface';
import { TablePaginationConfig, TableProps } from 'antd/lib';
import deleteAlerts from 'api/alerts/delete';
import get from 'api/alerts/get';
import getAll from 'api/alerts/getAll';
import patchAlert from 'api/alerts/patch';
import ruleStats from 'api/alerts/ruleStats';
import save from 'api/alerts/save';
import timelineGraph from 'api/alerts/timelineGraph';
import timelineTable from 'api/alerts/timelineTable';
import topContributors from 'api/alerts/topContributors';
import { TabRoutes } from 'components/RouteTab/types';
import { QueryParams } from 'constants/query';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
import AlertHistory from 'container/AlertHistory';
import { TIMELINE_TABLE_PAGE_SIZE } from 'container/AlertHistory/constants';
import { AlertDetailsTab, TimelineFilter } from 'container/AlertHistory/types';
import { urlKey } from 'container/AllError/utils';
import useAxiosError from 'hooks/useAxiosError';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import GetMinMax from 'lib/getMinMax';
import history from 'lib/history';
import { History, Table } from 'lucide-react';
import EditRules from 'pages/EditRules';
import { OrderPreferenceItems } from 'pages/Logs/config';
import PaginationInfoText from 'periscope/components/PaginationInfoText/PaginationInfoText';
import { useAlertRule } from 'providers/Alert';
import { useCallback, useMemo } from 'react';
import { useMutation, useQuery, useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
import { generatePath, useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
AlertDef,
AlertRuleStatsPayload,
AlertRuleTimelineGraphResponsePayload,
AlertRuleTimelineTableResponse,
AlertRuleTimelineTableResponsePayload,
AlertRuleTopContributorsPayload,
} from 'types/api/alerts/def';
import { PayloadProps } from 'types/api/alerts/get';
import { GlobalReducer } from 'types/reducer/globalTime';
import { nanoToMilli } from 'utils/timeUtils';
export const useAlertHistoryQueryParams = (): {
ruleId: string | null;
startTime: number;
endTime: number;
hasStartAndEndParams: boolean;
params: URLSearchParams;
} => {
const params = useUrlQuery();
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const startTime = params.get(QueryParams.startTime);
const endTime = params.get(QueryParams.endTime);
const intStartTime = parseInt(startTime || '0', 10);
const intEndTime = parseInt(endTime || '0', 10);
const hasStartAndEndParams = !!intStartTime && !!intEndTime;
const { maxTime, minTime } = useMemo(() => {
if (hasStartAndEndParams)
return GetMinMax('custom', [intStartTime, intEndTime]);
return GetMinMax(globalTime.selectedTime);
}, [hasStartAndEndParams, intStartTime, intEndTime, globalTime.selectedTime]);
const ruleId = params.get(QueryParams.ruleId);
return {
ruleId,
startTime: Math.floor(nanoToMilli(minTime)),
endTime: Math.floor(nanoToMilli(maxTime)),
hasStartAndEndParams,
params,
};
};
export const useRouteTabUtils = (): { routes: TabRoutes[] } => {
const urlQuery = useUrlQuery();
const getRouteUrl = (tab: AlertDetailsTab): string => {
let route = '';
let params = urlQuery.toString();
const ruleIdKey = QueryParams.ruleId;
const relativeTimeKey = QueryParams.relativeTime;
switch (tab) {
case AlertDetailsTab.OVERVIEW:
route = ROUTES.ALERT_OVERVIEW;
break;
case AlertDetailsTab.HISTORY:
params = `${ruleIdKey}=${urlQuery.get(
ruleIdKey,
)}&${relativeTimeKey}=${urlQuery.get(relativeTimeKey)}`;
route = ROUTES.ALERT_HISTORY;
break;
default:
return '';
}
return `${generatePath(route)}?${params}`;
};
const routes = [
{
Component: EditRules,
name: (
<div className="tab-item">
<Table size={14} />
Overview
</div>
),
route: getRouteUrl(AlertDetailsTab.OVERVIEW),
key: ROUTES.ALERT_OVERVIEW,
},
{
Component: AlertHistory,
name: (
<div className="tab-item">
<History size={14} />
History
</div>
),
route: getRouteUrl(AlertDetailsTab.HISTORY),
key: ROUTES.ALERT_HISTORY,
},
];
return { routes };
};
type Props = {
ruleId: string | null;
isValidRuleId: boolean;
alertDetailsResponse:
| SuccessResponse<PayloadProps, unknown>
| ErrorResponse
| undefined;
isLoading: boolean;
isRefetching: boolean;
isError: boolean;
};
export const useGetAlertRuleDetails = (): Props => {
const { ruleId } = useAlertHistoryQueryParams();
const isValidRuleId = ruleId !== null && String(ruleId).length !== 0;
const {
isLoading,
data: alertDetailsResponse,
isRefetching,
isError,
} = useQuery([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId], {
queryFn: () =>
get({
id: parseInt(ruleId || '', 10),
}),
enabled: isValidRuleId,
refetchOnMount: false,
refetchOnWindowFocus: false,
});
return {
ruleId,
isLoading,
alertDetailsResponse,
isRefetching,
isError,
isValidRuleId,
};
};
type GetAlertRuleDetailsApiProps = {
isLoading: boolean;
isRefetching: boolean;
isError: boolean;
isValidRuleId: boolean;
ruleId: string | null;
};
type GetAlertRuleDetailsStatsProps = GetAlertRuleDetailsApiProps & {
data:
| SuccessResponse<AlertRuleStatsPayload, unknown>
| ErrorResponse
| undefined;
};
export const useGetAlertRuleDetailsStats = (): GetAlertRuleDetailsStatsProps => {
const { ruleId, startTime, endTime } = useAlertHistoryQueryParams();
const isValidRuleId = ruleId !== null && String(ruleId).length !== 0;
const { isLoading, isRefetching, isError, data } = useQuery(
[REACT_QUERY_KEY.ALERT_RULE_STATS, ruleId, startTime, endTime],
{
queryFn: () =>
ruleStats({
id: parseInt(ruleId || '', 10),
start: startTime,
end: endTime,
}),
enabled: isValidRuleId && !!startTime && !!endTime,
refetchOnMount: false,
refetchOnWindowFocus: false,
},
);
return { isLoading, isRefetching, isError, data, isValidRuleId, ruleId };
};
type GetAlertRuleDetailsTopContributorsProps = GetAlertRuleDetailsApiProps & {
data:
| SuccessResponse<AlertRuleTopContributorsPayload, unknown>
| ErrorResponse
| undefined;
};
export const useGetAlertRuleDetailsTopContributors = (): GetAlertRuleDetailsTopContributorsProps => {
const { ruleId, startTime, endTime } = useAlertHistoryQueryParams();
const isValidRuleId = ruleId !== null && String(ruleId).length !== 0;
const { isLoading, isRefetching, isError, data } = useQuery(
[REACT_QUERY_KEY.ALERT_RULE_TOP_CONTRIBUTORS, ruleId, startTime, endTime],
{
queryFn: () =>
topContributors({
id: parseInt(ruleId || '', 10),
start: startTime,
end: endTime,
}),
enabled: isValidRuleId,
refetchOnMount: false,
refetchOnWindowFocus: false,
},
);
return { isLoading, isRefetching, isError, data, isValidRuleId, ruleId };
};
type GetAlertRuleDetailsTimelineTableProps = GetAlertRuleDetailsApiProps & {
data:
| SuccessResponse<AlertRuleTimelineTableResponsePayload, unknown>
| ErrorResponse
| undefined;
};
export const useGetAlertRuleDetailsTimelineTable = (): GetAlertRuleDetailsTimelineTableProps => {
const { ruleId, startTime, endTime, params } = useAlertHistoryQueryParams();
const { updatedOrder, offset } = useMemo(
() => ({
updatedOrder: params.get(urlKey.order) ?? OrderPreferenceItems.ASC,
offset: parseInt(params.get(urlKey.offset) ?? '1', 10),
}),
[params],
);
const timelineFilter = params.get('timelineFilter');
const isValidRuleId = ruleId !== null && String(ruleId).length !== 0;
const hasStartAndEnd = startTime !== null && endTime !== null;
const { isLoading, isRefetching, isError, data } = useQuery(
[
REACT_QUERY_KEY.ALERT_RULE_TIMELINE_TABLE,
ruleId,
startTime,
endTime,
timelineFilter,
updatedOrder,
offset,
],
{
queryFn: () =>
timelineTable({
id: parseInt(ruleId || '', 10),
start: startTime,
end: endTime,
limit: TIMELINE_TABLE_PAGE_SIZE,
order: updatedOrder,
offset,
...(timelineFilter && timelineFilter !== TimelineFilter.ALL
? {
state: timelineFilter === TimelineFilter.FIRED ? 'firing' : 'normal',
}
: {}),
}),
enabled: isValidRuleId && hasStartAndEnd,
refetchOnMount: false,
refetchOnWindowFocus: false,
},
);
return { isLoading, isRefetching, isError, data, isValidRuleId, ruleId };
};
export const useTimelineTable = ({
totalItems,
}: {
totalItems: number;
}): {
paginationConfig: TablePaginationConfig;
onChangeHandler: (
pagination: TablePaginationConfig,
sorter: any,
filters: any,
extra: any,
) => void;
} => {
const { pathname } = useLocation();
const { search } = useLocation();
const params = useMemo(() => new URLSearchParams(search), [search]);
const offset = params.get('offset') ?? '0';
const onChangeHandler: TableProps<AlertRuleTimelineTableResponse>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter:
| SorterResult<AlertRuleTimelineTableResponse>[]
| SorterResult<AlertRuleTimelineTableResponse>,
) => {
if (!Array.isArray(sorter)) {
const { pageSize = 0, current = 0 } = pagination;
const { order } = sorter;
const updatedOrder = order === 'ascend' ? 'asc' : 'desc';
const params = new URLSearchParams(window.location.search);
history.replace(
`${pathname}?${createQueryParams({
...Object.fromEntries(params),
order: updatedOrder,
offset: current * TIMELINE_TABLE_PAGE_SIZE - TIMELINE_TABLE_PAGE_SIZE,
pageSize,
})}`,
);
}
},
[pathname],
);
const offsetInt = parseInt(offset, 10);
const pageSize = params.get('pageSize') ?? String(TIMELINE_TABLE_PAGE_SIZE);
const pageSizeInt = parseInt(pageSize, 10);
const paginationConfig: TablePaginationConfig = {
pageSize: pageSizeInt,
showTotal: PaginationInfoText,
current: offsetInt / TIMELINE_TABLE_PAGE_SIZE + 1,
showSizeChanger: false,
hideOnSinglePage: true,
total: totalItems,
};
return { paginationConfig, onChangeHandler };
};
export const useAlertRuleStatusToggle = ({
ruleId,
}: {
ruleId: string;
}): {
handleAlertStateToggle: (state: boolean) => void;
} => {
const { isAlertRuleDisabled, setIsAlertRuleDisabled } = useAlertRule();
const { notifications } = useNotifications();
const queryClient = useQueryClient();
const handleError = useAxiosError();
const { mutate: toggleAlertState } = useMutation(
[REACT_QUERY_KEY.TOGGLE_ALERT_STATE, ruleId],
patchAlert,
{
onMutate: () => {
setIsAlertRuleDisabled((prev) => !prev);
},
onSuccess: () => {
notifications.success({
message: `Alert has been ${isAlertRuleDisabled ? 'enabled' : 'disabled'}.`,
});
},
onError: (error) => {
queryClient.refetchQueries([REACT_QUERY_KEY.ALERT_RULE_DETAILS]);
handleError(error);
},
},
);
const handleAlertStateToggle = (): void => {
const args = {
id: parseInt(ruleId, 10),
data: { disabled: !isAlertRuleDisabled },
};
toggleAlertState(args);
};
return { handleAlertStateToggle };
};
export const useAlertRuleDuplicate = ({
alertDetails,
}: {
alertDetails: AlertDef;
}): {
handleAlertDuplicate: () => void;
} => {
const { notifications } = useNotifications();
const params = useUrlQuery();
const { refetch } = useQuery(REACT_QUERY_KEY.GET_ALL_ALLERTS, {
queryFn: getAll,
cacheTime: 0,
});
const handleError = useAxiosError();
const { mutate: duplicateAlert } = useMutation(
[REACT_QUERY_KEY.DUPLICATE_ALERT_RULE],
save,
{
onSuccess: async () => {
notifications.success({
message: `Success`,
});
const { data: allAlertsData } = await refetch();
if (
allAlertsData &&
allAlertsData.payload &&
allAlertsData.payload.length > 0
) {
const clonedAlert =
allAlertsData.payload[allAlertsData.payload.length - 1];
params.set(QueryParams.ruleId, String(clonedAlert.id));
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
}
},
onError: handleError,
},
);
const handleAlertDuplicate = (): void => {
const args = {
data: { ...alertDetails, alert: alertDetails.alert?.concat(' - Copy') },
};
duplicateAlert(args);
};
return { handleAlertDuplicate };
};
export const useAlertRuleDelete = ({
ruleId,
}: {
ruleId: number;
}): {
handleAlertDelete: () => void;
} => {
const { notifications } = useNotifications();
const handleError = useAxiosError();
const { mutate: deleteAlert } = useMutation(
[REACT_QUERY_KEY.REMOVE_ALERT_RULE, ruleId],
deleteAlerts,
{
onSuccess: async () => {
notifications.success({
message: `Success`,
});
history.push(ROUTES.LIST_ALL_ALERT);
},
onError: handleError,
},
);
const handleAlertDelete = (): void => {
const args = { id: ruleId };
deleteAlert(args);
};
return { handleAlertDelete };
};
type GetAlertRuleDetailsTimelineGraphProps = GetAlertRuleDetailsApiProps & {
data:
| SuccessResponse<AlertRuleTimelineGraphResponsePayload, unknown>
| ErrorResponse
| undefined;
};
export const useGetAlertRuleDetailsTimelineGraphData = (): GetAlertRuleDetailsTimelineGraphProps => {
const { ruleId, startTime, endTime } = useAlertHistoryQueryParams();
const isValidRuleId = ruleId !== null && String(ruleId).length !== 0;
const hasStartAndEnd = startTime !== null && endTime !== null;
const { isLoading, isRefetching, isError, data } = useQuery(
[REACT_QUERY_KEY.ALERT_RULE_TIMELINE_GRAPH, ruleId, startTime, endTime],
{
queryFn: () =>
timelineGraph({
id: parseInt(ruleId || '', 10),
start: startTime,
end: endTime,
}),
enabled: isValidRuleId && hasStartAndEnd,
refetchOnMount: false,
refetchOnWindowFocus: false,
},
);
return { isLoading, isRefetching, isError, data, isValidRuleId, ruleId };
};