mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-13 20:30:25 +01:00
Compare commits
6 Commits
main
...
downtime-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
062b21727f | ||
|
|
337bf469c0 | ||
|
|
cbb53972b5 | ||
|
|
e87b339755 | ||
|
|
297a6ecec6 | ||
|
|
fa9e2f6811 |
@@ -14,7 +14,7 @@ import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQue
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { AlertListTabs } from 'pages/AlertList/types';
|
||||
import { GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
|
||||
import { CalendarClock, GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
@@ -175,11 +175,21 @@ function CreateRules(): JSX.Element {
|
||||
{
|
||||
label: (
|
||||
<div className="periscope-tab top-level-tab">
|
||||
<ConfigureIcon width={14} height={14} />
|
||||
Configuration
|
||||
<CalendarClock size={14} />
|
||||
Planned Downtime
|
||||
</div>
|
||||
),
|
||||
key: AlertListTabs.CONFIGURATION,
|
||||
key: AlertListTabs.PLANNED_DOWNTIME,
|
||||
children: null,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="periscope-tab top-level-tab">
|
||||
<ConfigureIcon width={14} height={14} />
|
||||
Routing Policies
|
||||
</div>
|
||||
),
|
||||
key: AlertListTabs.ROUTING_POLICIES,
|
||||
children: null,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -4,5 +4,4 @@ export const THRESHOLD_TAB_TOOLTIP =
|
||||
export const ANOMALY_TAB_TOOLTIP =
|
||||
'An alert is triggered whenever the metric deviates from an expected pattern.';
|
||||
|
||||
export const ROUTING_POLICIES_ROUTE =
|
||||
'/alerts?tab=Configuration&subTab=routing-policies';
|
||||
export const ROUTING_POLICIES_ROUTE = '/alerts?tab=RoutingPolicies';
|
||||
|
||||
@@ -5,11 +5,12 @@ import { Collapse, Flex, Space, Table, TableProps, Tooltip } from 'antd';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { DefaultOptionType } from 'antd/es/select';
|
||||
import type {
|
||||
ListDowntimeSchedules200,
|
||||
RenderErrorResponseDTO,
|
||||
AlertmanagertypesPlannedMaintenanceDTO,
|
||||
AlertmanagertypesScheduleDTO,
|
||||
import {
|
||||
AlertmanagertypesMaintenanceStatusDTO,
|
||||
type ListDowntimeSchedules200,
|
||||
type RenderErrorResponseDTO,
|
||||
type AlertmanagertypesPlannedMaintenanceDTO,
|
||||
type AlertmanagertypesScheduleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { ErrorType } from 'api/generatedAPIInstance';
|
||||
import cx from 'classnames';
|
||||
@@ -32,6 +33,50 @@ import './PlannedDowntime.styles.scss';
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
const STATUS_BADGE_PROPS: Record<
|
||||
AlertmanagertypesMaintenanceStatusDTO,
|
||||
{ color: 'forest' | 'robin' | 'vanilla'; label: string }
|
||||
> = {
|
||||
[AlertmanagertypesMaintenanceStatusDTO.active]: {
|
||||
color: 'forest',
|
||||
label: 'Active',
|
||||
},
|
||||
[AlertmanagertypesMaintenanceStatusDTO.upcoming]: {
|
||||
color: 'robin',
|
||||
label: 'Upcoming',
|
||||
},
|
||||
[AlertmanagertypesMaintenanceStatusDTO.expired]: {
|
||||
color: 'vanilla',
|
||||
label: 'Expired',
|
||||
},
|
||||
};
|
||||
|
||||
const STATUS_SORT_ORDER: Record<AlertmanagertypesMaintenanceStatusDTO, number> =
|
||||
{
|
||||
[AlertmanagertypesMaintenanceStatusDTO.active]: 0,
|
||||
[AlertmanagertypesMaintenanceStatusDTO.upcoming]: 1,
|
||||
[AlertmanagertypesMaintenanceStatusDTO.expired]: 2,
|
||||
};
|
||||
|
||||
function StatusBadge({
|
||||
status,
|
||||
}: {
|
||||
status?: AlertmanagertypesMaintenanceStatusDTO;
|
||||
}): JSX.Element | null {
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
const props = STATUS_BADGE_PROPS[status];
|
||||
if (!props) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Badge color={props.color} variant="outline">
|
||||
{props.label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
interface AlertRuleTagsProps {
|
||||
selectedTags: DefaultOptionType | DefaultOptionType[];
|
||||
closable: boolean;
|
||||
@@ -83,11 +128,13 @@ export function AlertRuleTags(props: AlertRuleTagsProps): JSX.Element {
|
||||
function HeaderComponent({
|
||||
name,
|
||||
duration,
|
||||
status,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
}: {
|
||||
name: string;
|
||||
duration: string;
|
||||
status?: AlertmanagertypesMaintenanceStatusDTO;
|
||||
handleEdit: () => void;
|
||||
handleDelete: () => void;
|
||||
}): JSX.Element {
|
||||
@@ -95,9 +142,10 @@ function HeaderComponent({
|
||||
const isCrudEnabled = user?.role !== USER_ROLES.VIEWER;
|
||||
return (
|
||||
<Flex className="header-content" justify="space-between">
|
||||
<Flex gap={8}>
|
||||
<Flex gap={8} align="center">
|
||||
<Typography>{name}</Typography>
|
||||
<Badge color="vanilla">{duration}</Badge>
|
||||
<StatusBadge status={status} />
|
||||
</Flex>
|
||||
|
||||
{isCrudEnabled && (
|
||||
@@ -225,6 +273,7 @@ export function CustomCollapseList(
|
||||
createdAt,
|
||||
createdBy,
|
||||
schedule,
|
||||
status,
|
||||
updatedAt,
|
||||
updatedBy,
|
||||
name,
|
||||
@@ -253,6 +302,7 @@ export function CustomCollapseList(
|
||||
: getDuration(schedule?.startTime || '', schedule?.endTime || '')
|
||||
}
|
||||
name={defaultTo(name, '')}
|
||||
status={status}
|
||||
handleEdit={() => {
|
||||
setInitialValues({ ...props });
|
||||
setModalOpen(true);
|
||||
@@ -326,6 +376,11 @@ export function PlannedDowntimeList({
|
||||
|
||||
const tableData = [...(downtimeSchedules.data?.data || [])]
|
||||
.sort((a, b): number => {
|
||||
const statusDiff =
|
||||
(STATUS_SORT_ORDER[a.status] ?? 99) - (STATUS_SORT_ORDER[b.status] ?? 99);
|
||||
if (statusDiff !== 0) {
|
||||
return statusDiff;
|
||||
}
|
||||
if (a?.updatedAt && b?.updatedAt) {
|
||||
return dayjs(b.updatedAt).diff(dayjs(a.updatedAt));
|
||||
}
|
||||
|
||||
@@ -7,11 +7,10 @@ const TAB_SELECTOR = '.ant-tabs-tab';
|
||||
const LIST_ALERT_RULES_TEXT = 'List Alert Rules Component';
|
||||
const TRIGGERED_ALERTS_TEXT = 'Triggered Alerts';
|
||||
const ALERT_RULES_TEXT = 'Alert Rules';
|
||||
const CONFIGURATION_TEXT = 'Configuration';
|
||||
const PLANNED_DOWNTIME_TEXT = 'Planned Downtime';
|
||||
const ROUTING_POLICIES_TEXT = 'Routing Policies';
|
||||
const PLANNED_DOWNTIME_SUB_TAB = 'planned-downtime';
|
||||
const ROUTING_POLICIES_SUB_TAB = 'routing-policies';
|
||||
const PLANNED_DOWNTIME_TAB = 'PlannedDowntime';
|
||||
const ROUTING_POLICIES_TAB = 'RoutingPolicies';
|
||||
|
||||
const mockUseLocation = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
@@ -106,7 +105,7 @@ describe('AlertList', () => {
|
||||
expect(screen.getByText(LIST_ALERT_RULES_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all three main tabs', () => {
|
||||
it('should render all four top-level tabs', () => {
|
||||
mockQueryParams({});
|
||||
mockLocation(ALERTS_PATH);
|
||||
|
||||
@@ -114,7 +113,8 @@ describe('AlertList', () => {
|
||||
|
||||
expect(screen.getByText(TRIGGERED_ALERTS_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(ALERT_RULES_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(CONFIGURATION_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(PLANNED_DOWNTIME_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(ROUTING_POLICIES_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -137,13 +137,22 @@ describe('AlertList', () => {
|
||||
expect(screen.getByText(LIST_ALERT_RULES_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render Configuration tab with default Planned Downtime sub-tab when tab query param is Configuration', () => {
|
||||
mockQueryParams({ tab: 'Configuration' });
|
||||
it('should render PlannedDowntime tab when tab query param is PlannedDowntime', () => {
|
||||
mockQueryParams({ tab: PLANNED_DOWNTIME_TAB });
|
||||
mockLocation(ALERTS_PATH);
|
||||
|
||||
render(<AlertList />);
|
||||
|
||||
expect(screen.getByText(PLANNED_DOWNTIME_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText('Planned Downtime Component')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render RoutingPolicies tab when tab query param is RoutingPolicies', () => {
|
||||
mockQueryParams({ tab: ROUTING_POLICIES_TAB });
|
||||
mockLocation(ALERTS_PATH);
|
||||
|
||||
render(<AlertList />);
|
||||
|
||||
expect(screen.getByText('Routing Policies Component')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should navigate to TriggeredAlerts tab when clicked', () => {
|
||||
@@ -157,84 +166,30 @@ describe('AlertList', () => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith('/alerts?tab=TriggeredAlerts');
|
||||
});
|
||||
|
||||
it('should navigate to AlertRules tab when clicked', () => {
|
||||
mockQueryParams({ tab: 'TriggeredAlerts' });
|
||||
it('should navigate to PlannedDowntime tab when clicked', () => {
|
||||
mockQueryParams({ tab: 'AlertRules' });
|
||||
mockLocation(ALERTS_PATH);
|
||||
|
||||
render(<AlertList />);
|
||||
|
||||
clickTab(ALERT_RULES_TEXT);
|
||||
clickTab(PLANNED_DOWNTIME_TEXT);
|
||||
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith('/alerts?tab=AlertRules');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration Tab', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render Configuration tab with default Planned Downtime sub-tab', () => {
|
||||
mockQueryParams({ tab: CONFIGURATION_TEXT });
|
||||
mockLocation(ALERTS_PATH);
|
||||
|
||||
render(<AlertList />);
|
||||
|
||||
expect(screen.getByText(PLANNED_DOWNTIME_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(ROUTING_POLICIES_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText('Planned Downtime Component')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render Routing Policies sub-tab when subTab query param is routing-policies', () => {
|
||||
mockQueryParams({
|
||||
tab: CONFIGURATION_TEXT,
|
||||
subTab: ROUTING_POLICIES_SUB_TAB,
|
||||
});
|
||||
mockLocation(ALERTS_PATH);
|
||||
|
||||
render(<AlertList />);
|
||||
|
||||
expect(screen.getByText('Routing Policies Component')).toBeInTheDocument();
|
||||
});
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
`/alerts?tab=${PLANNED_DOWNTIME_TAB}`,
|
||||
);
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should navigate to Configuration tab with default subTab when clicked', () => {
|
||||
mockQueryParams({ tab: 'AlertRules' });
|
||||
mockLocation(ALERTS_PATH);
|
||||
it('should navigate to RoutingPolicies tab when clicked', () => {
|
||||
mockQueryParams({ tab: 'AlertRules' });
|
||||
mockLocation(ALERTS_PATH);
|
||||
|
||||
render(<AlertList />);
|
||||
render(<AlertList />);
|
||||
|
||||
clickTab(CONFIGURATION_TEXT);
|
||||
clickTab(ROUTING_POLICIES_TEXT);
|
||||
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
`/alerts?tab=Configuration&subTab=${PLANNED_DOWNTIME_SUB_TAB}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should preserve existing subTab when navigating to Configuration tab', () => {
|
||||
mockQueryParams({ tab: 'AlertRules', subTab: ROUTING_POLICIES_SUB_TAB });
|
||||
mockLocation(ALERTS_PATH);
|
||||
|
||||
render(<AlertList />);
|
||||
|
||||
clickTab(CONFIGURATION_TEXT);
|
||||
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
`/alerts?tab=Configuration&subTab=${ROUTING_POLICIES_SUB_TAB}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear subTab when navigating away from Configuration tab', () => {
|
||||
mockQueryParams({
|
||||
tab: CONFIGURATION_TEXT,
|
||||
subTab: PLANNED_DOWNTIME_SUB_TAB,
|
||||
});
|
||||
mockLocation(ALERTS_PATH);
|
||||
|
||||
render(<AlertList />);
|
||||
|
||||
clickTab(ALERT_RULES_TEXT);
|
||||
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith('/alerts?tab=AlertRules');
|
||||
});
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
`/alerts?tab=${ROUTING_POLICIES_TAB}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Tabs, TabsProps } from 'antd';
|
||||
import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon';
|
||||
@@ -10,10 +9,10 @@ import RoutingPolicies from 'container/RoutingPolicies';
|
||||
import TriggeredAlerts from 'container/TriggeredAlerts';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
|
||||
import { CalendarClock, GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
|
||||
import AlertDetails from 'pages/AlertDetails';
|
||||
|
||||
import { AlertListSubTabs, AlertListTabs } from './types';
|
||||
import { AlertListTabs } from './types';
|
||||
|
||||
import './AlertList.styles.scss';
|
||||
|
||||
@@ -23,44 +22,9 @@ function AllAlertList(): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const tab = urlQuery.get('tab');
|
||||
const subTab = urlQuery.get('subTab');
|
||||
const isAlertHistory = location.pathname === ROUTES.ALERT_HISTORY;
|
||||
const isAlertOverview = location.pathname === ROUTES.ALERT_OVERVIEW;
|
||||
|
||||
const handleConfigurationTabChange = useCallback(
|
||||
(subTab: string): void => {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
queryParams.set('tab', AlertListTabs.CONFIGURATION);
|
||||
queryParams.set('subTab', subTab);
|
||||
safeNavigate(`/alerts?${queryParams.toString()}`);
|
||||
},
|
||||
[safeNavigate],
|
||||
);
|
||||
|
||||
const configurationTab = useMemo(() => {
|
||||
const tabs = [
|
||||
{
|
||||
label: 'Planned Downtime',
|
||||
key: AlertListSubTabs.PLANNED_DOWNTIME,
|
||||
children: <PlannedDowntime />,
|
||||
},
|
||||
{
|
||||
label: 'Routing Policies',
|
||||
key: AlertListSubTabs.ROUTING_POLICIES,
|
||||
children: <RoutingPolicies />,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Tabs
|
||||
className="configuration-tabs"
|
||||
activeKey={subTab || AlertListSubTabs.PLANNED_DOWNTIME}
|
||||
items={tabs}
|
||||
onChange={handleConfigurationTabChange}
|
||||
/>
|
||||
);
|
||||
}, [subTab, handleConfigurationTabChange]);
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
label: (
|
||||
@@ -89,12 +53,22 @@ function AllAlertList(): JSX.Element {
|
||||
{
|
||||
label: (
|
||||
<div className="periscope-tab top-level-tab">
|
||||
<ConfigureIcon width={14} height={14} />
|
||||
Configuration
|
||||
<CalendarClock size={14} />
|
||||
Planned Downtime
|
||||
</div>
|
||||
),
|
||||
key: AlertListTabs.CONFIGURATION,
|
||||
children: configurationTab,
|
||||
key: AlertListTabs.PLANNED_DOWNTIME,
|
||||
children: <PlannedDowntime />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="periscope-tab top-level-tab">
|
||||
<ConfigureIcon width={14} height={14} />
|
||||
Routing Policies
|
||||
</div>
|
||||
),
|
||||
key: AlertListTabs.ROUTING_POLICIES,
|
||||
children: <RoutingPolicies />,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -105,18 +79,7 @@ function AllAlertList(): JSX.Element {
|
||||
activeKey={tab || AlertListTabs.ALERT_RULES}
|
||||
onChange={(tab): void => {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
queryParams.set('tab', tab);
|
||||
|
||||
// If navigating to Configuration tab, set default subTab
|
||||
if (tab === AlertListTabs.CONFIGURATION) {
|
||||
const currentSubTab = subTab || AlertListSubTabs.PLANNED_DOWNTIME;
|
||||
queryParams.set('subTab', currentSubTab);
|
||||
} else {
|
||||
// Clear subTab when navigating out of Configuration tab
|
||||
queryParams.delete('subTab');
|
||||
}
|
||||
|
||||
safeNavigate(`/alerts?${queryParams.toString()}`);
|
||||
}}
|
||||
className={`alerts-container ${
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
export enum AlertListSubTabs {
|
||||
PLANNED_DOWNTIME = 'planned-downtime',
|
||||
ROUTING_POLICIES = 'routing-policies',
|
||||
}
|
||||
|
||||
export enum AlertListTabs {
|
||||
TRIGGERED_ALERTS = 'TriggeredAlerts',
|
||||
ALERT_RULES = 'AlertRules',
|
||||
CONFIGURATION = 'Configuration',
|
||||
PLANNED_DOWNTIME = 'PlannedDowntime',
|
||||
ROUTING_POLICIES = 'RoutingPolicies',
|
||||
}
|
||||
|
||||
@@ -163,17 +163,29 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
|
||||
}
|
||||
|
||||
func (r *maintenance) DeletePlannedMaintenance(ctx context.Context, id valuer.UUID) error {
|
||||
_, err := r.sqlstore.
|
||||
BunDB().
|
||||
NewDelete().
|
||||
Model(new(alertmanagertypes.StorablePlannedMaintenance)).
|
||||
Where("id = ?", id.StringValue()).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return r.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "cannot delete planned maintenance because it is referenced by associated rules, remove the rules from the planned maintenance first")
|
||||
}
|
||||
return r.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
|
||||
_, err := r.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model(new(alertmanagertypes.StorablePlannedMaintenanceRule)).
|
||||
Where("planned_maintenance_id = ?", id.StringValue()).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
_, err = r.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model(new(alertmanagertypes.StorablePlannedMaintenance)).
|
||||
Where("id = ?", id.StringValue()).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance *alertmanagertypes.PostablePlannedMaintenance, id valuer.UUID) error {
|
||||
|
||||
Reference in New Issue
Block a user