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 { RelativeTimeMap } from 'container/TopNav/DateTimeSelection/config'; 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 BetaTag from 'periscope/components/BetaTag/BetaTag'; 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 { generatePath, useLocation } from 'react-router-dom'; 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 { nanoToMilli } from 'utils/timeUtils'; export const useAlertHistoryQueryParams = (): { ruleId: string | null; startTime: number; endTime: number; hasStartAndEndParams: boolean; params: URLSearchParams; } => { const params = useUrlQuery(); const startTime = params.get(QueryParams.startTime); const endTime = params.get(QueryParams.endTime); const relativeTime = params.get(QueryParams.relativeTime) ?? RelativeTimeMap['6hr']; 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(relativeTime); }, [hasStartAndEndParams, intStartTime, intEndTime, relativeTime]); 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: (
Overview ), route: getRouteUrl(AlertDetailsTab.OVERVIEW), key: ROUTES.ALERT_OVERVIEW, }, { Component: AlertHistory, name: (
History
), route: getRouteUrl(AlertDetailsTab.HISTORY), key: ROUTES.ALERT_HISTORY, }, ]; return { routes }; }; type Props = { ruleId: string | null; isValidRuleId: boolean; alertDetailsResponse: | SuccessResponse | 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 | 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 | 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 | 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) ?? '0', 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['onChange'] = useCallback( ( pagination: TablePaginationConfig, filters: Record, sorter: | SorterResult[] | SorterResult, ) => { 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 | 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 }; };