Compare commits

..

15 Commits

Author SHA1 Message Date
Jatinderjit Singh
36417a5f9e test: cover recurring schedule active window in IsActive
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 23:44:39 +05:30
Jatinderjit Singh
989b1252df test: cover fixed schedule active window in IsActive
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 23:22:36 +05:30
Jatinderjit Singh
51cb119f79 fix: make startTime a required field 2026-05-29 22:50:06 +05:30
Jatinderjit Singh
180a2c067f refactor: remove redundant code 2026-05-29 22:32:17 +05:30
Jatinderjit Singh
83351ca01d fix: use embedded timezone in start/end times
Accept times in any timezone, but always convert them to the selected
timezone. The conversion is required to correctly handle the recurring
maintenances for timezones where DST is involved.
2026-05-29 21:18:45 +05:30
Jatinderjit Singh
b11e2af392 fix: remove recurrence.startTime/endTime usages 2026-05-29 21:18:45 +05:30
Jatinderjit Singh
7f6e89ea22 fix: upcoming check for recurring maintenances 2026-05-29 21:18:45 +05:30
Jatinderjit Singh
8aeb9b5a77 refactor: code cleanup 2026-05-29 21:18:45 +05:30
Jatinderjit Singh
46c8f3579e refactor(alertmanager): drop start/end bounds from Recurrence
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 18:39:40 +05:30
Jatinderjit Singh
9ff045482f feat(alertmanager): migrate recurrence bounds to schedule level
Promote startTime/endTime from a planned maintenance's nested recurrence
up to the schedule level. For recurring maintenances the recurrence
bounds were the source of truth; the recurrence struct loses these
fields in the next step, so the values are moved while they can still be
read. The migration operates on raw JSON for that reason.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 17:21:00 +05:30
swapnil-signoz
910645516d chore: remove cloud integration service cascade delete constraint (#11480)
* chore: remove cloud integration service cascade delete constraint

* refactor: manually recreate table
2026-05-29 11:21:05 +00:00
Tushar Vats
edc1278769 fix(querybuilder): return PreparedWhereClause by value so warnings propagate when clause is empty (#11395)
* fix: propage dropped warnings from where clause visitor

* fix: added integration test

* fix: make py-fmt

* fix: remove stale comment
2026-05-29 10:09:09 +00:00
Yunus M
da1b09c479 refactor: replace antd Tabs with @signozhq/ui Tabs (#11392)
* refactor: replace antd Tabs with @signozhq/ui Tabs

Migrates Tabs usage from antd to the @signozhq/ui Tabs component across
dashboard settings, integration details, metrics application, pipelines,
trace detail, and workspace-locked pages. API differences (activeKey →
value, defaultActiveKey → defaultValue, TabsProps['items'] → TabItemProps[])
are updated to match the new component.

* refactor: enhance RouteTab component with new styling and functionality
2026-05-29 09:53:51 +00:00
Gaurav Tewari
1f406823d8 refactor(frontend): migrate plain antd Input to @signozhq/ui/input (#11401)
* chore: refactor input

* chore: remove markdown

* chore: update markdown

* fix: minior comments for input

* chore: remove comments

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-05-29 08:37:54 +00:00
Nikhil Mantri
f626380b1a fix(infra-monitoring): align v2 custom queries' bounds with QBv5 querier step adjustment (#11397)
* chore: updated logic and use centralized function in the module

* chore: filter metric groups

* chore: filter metric groups

* chore: formula correction

* chore: added step flooring note

* chore: comment correction

* chore: comment correction

* chore: removed function

* chore: renamed variables

* chore: fix for surfacing meta for pods custom group by

* chore: added todo and note

* chore: added changes based on comments

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-05-29 07:14:43 +00:00
148 changed files with 1484 additions and 3021 deletions

View File

@@ -309,10 +309,6 @@ components:
properties:
duration:
type: string
endTime:
format: date-time
nullable: true
type: string
repeatOn:
items:
$ref: '#/components/schemas/AlertmanagertypesRepeatOn'
@@ -320,11 +316,7 @@ components:
type: array
repeatType:
$ref: '#/components/schemas/AlertmanagertypesRepeatType'
startTime:
format: date-time
type: string
required:
- startTime
- duration
- repeatType
type: object
@@ -358,6 +350,7 @@ components:
type: string
required:
- timezone
- startTime
type: object
AuthtypesAttributeMapping:
properties:
@@ -5945,22 +5938,6 @@ components:
- start
- end
type: object
RuletypesActiveMute:
properties:
description:
type: string
end:
format: date-time
nullable: true
type: string
id:
type: string
name:
type: string
start:
format: date-time
type: string
type: object
RuletypesAlertCompositeQuery:
properties:
panelType:
@@ -6242,11 +6219,6 @@ components:
additionalProperties:
type: string
type: object
mutes:
items:
$ref: '#/components/schemas/RuletypesActiveMute'
nullable: true
type: array
notificationSettings:
$ref: '#/components/schemas/RuletypesNotificationSettings'
preferredChannels:

View File

@@ -162,21 +162,11 @@ export interface AlertmanagertypesRecurrenceDTO {
* @type string
*/
duration: string;
/**
* @type string,null
* @format date-time
*/
endTime?: string | null;
/**
* @type array,null
*/
repeatOn?: AlertmanagertypesRepeatOnDTO[] | null;
repeatType: AlertmanagertypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: string;
}
export interface AlertmanagertypesScheduleDTO {
@@ -190,7 +180,7 @@ export interface AlertmanagertypesScheduleDTO {
* @type string
* @format date-time
*/
startTime?: string;
startTime: string;
/**
* @type string
*/
@@ -7051,31 +7041,6 @@ export interface RulestatehistorytypesGettableRuleStateWindowDTO {
state: RuletypesAlertStateDTO;
}
export interface RuletypesActiveMuteDTO {
/**
* @type string
*/
description?: string;
/**
* @type string,null
* @format date-time
*/
end?: string | null;
/**
* @type string
*/
id?: string;
/**
* @type string
*/
name?: string;
/**
* @type string
* @format date-time
*/
start?: string;
}
export enum RuletypesPanelTypeDTO {
value = 'value',
table = 'table',
@@ -7444,10 +7409,6 @@ export interface RuletypesRuleDTO {
* @type object
*/
labels?: RuletypesRuleDTOLabels;
/**
* @type array,null
*/
mutes?: RuletypesActiveMuteDTO[] | null;
notificationSettings?: RuletypesNotificationSettingsDTO;
/**
* @type array

View File

@@ -41,14 +41,22 @@ $item-spacing: 8px;
width: 100%;
background: transparent;
border: none;
border-radius: 0;
box-shadow: none;
outline: none;
height: auto;
color: var(--l1-foreground);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
padding: 0;
&.ant-input:focus {
&:focus,
&:focus-visible,
&:hover {
border: none;
box-shadow: none;
outline: none;
}
&::placeholder {

View File

@@ -6,7 +6,7 @@ import {
useState,
} from 'react';
import { Color } from '@signozhq/design-tokens';
import { Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts';

View File

@@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Card, Form, Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Card, Form } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';

View File

@@ -1,5 +1,6 @@
import { useState } from 'react';
import { Button, Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { X } from '@signozhq/icons';

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Input, InputNumber, Popover, Tooltip } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, InputNumber, Popover, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import type { DefaultOptionType } from 'antd/es/select';
import cx from 'classnames';

View File

@@ -266,6 +266,14 @@
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
&:focus:not(:focus-visible),
&.ant-btn:focus:not(:focus-visible) {
border-color: var(--l2-border);
border-left-color: transparent;
outline: none;
box-shadow: none;
}
}
}
}
@@ -291,5 +299,21 @@
.cm-placeholder {
font-size: 12px !important;
}
$add-on-row-height: 38px;
.periscope-input-with-label {
.input {
.ant-select {
height: $add-on-row-height;
}
}
}
.input-with-label {
.input {
height: $add-on-row-height;
}
}
}
}

View File

@@ -4,6 +4,23 @@
padding: 12px;
gap: 12px;
border-bottom: 1px solid var(--l1-border);
.search {
input {
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
--input-focus-background: var(--l2-background);
&::placeholder {
color: var(--l3-foreground);
}
--input-font-size: 14px;
--input-border-color: var(--l1-border);
--input-focus-border-color: var(--primary-background);
--input-focus-outline-width: 0;
--input-focus-outline-offset: 0;
}
}
.filter-header-checkbox {
display: flex;
align-items: center;

View File

@@ -1,6 +1,7 @@
/* eslint-disable sonarjs/no-identical-functions */
import { Fragment, useMemo, useState } from 'react';
import { Button, Input, Skeleton } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Skeleton } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';

View File

@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { Button, Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button } from 'antd';
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';

View File

@@ -0,0 +1,12 @@
.route-tab-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 4px;
}
.route-tab-extra {
display: flex;
align-items: center;
}

View File

@@ -70,7 +70,7 @@ describe('RouteTab component', () => {
</Router>,
);
expect(history.location.pathname).toBe('/');
fireEvent.click(screen.getByRole('tab', { name: 'Tab2' }));
fireEvent.mouseDown(screen.getByRole('tab', { name: 'Tab2' }));
expect(history.location.pathname).toBe('/tab2');
});
@@ -87,7 +87,7 @@ describe('RouteTab component', () => {
/>
</Router>,
);
fireEvent.click(screen.getByRole('tab', { name: 'Tab2' }));
fireEvent.mouseDown(screen.getByRole('tab', { name: 'Tab2' }));
expect(onChangeHandler).toHaveBeenCalled();
});
});

View File

@@ -1,10 +1,17 @@
import './RouteTab.styles.scss';
import {
generatePath,
matchPath,
useLocation,
useParams,
} from 'react-router-dom';
import { Tabs, TabsProps } from 'antd';
import {
TabsContent,
TabsList,
TabsRoot,
TabsTrigger,
} from '@signozhq/ui/tabs';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import { RouteTabProps } from './types';
@@ -16,11 +23,13 @@ interface Params {
function RouteTab({
routes,
activeKey,
defaultActiveKey,
onChangeHandler,
history,
showRightSection,
...rest
}: RouteTabProps & TabsProps): JSX.Element {
showRightSection = true,
tabBarExtraContent,
hideTabBar = false,
}: RouteTabProps): JSX.Element {
const params = useParams<Params>();
const location = useLocation();
@@ -46,38 +55,38 @@ function RouteTab({
}
};
const items = routes.map(({ Component, name, route, key }) => ({
label: name,
key,
tabKey: route,
children: <Component />,
}));
const resolvedActiveKey = currentRoute?.key || activeKey;
const extraContent =
tabBarExtraContent ??
(showRightSection && (
<HeaderRightSection enableAnnouncements={false} enableShare enableFeedback />
));
return (
<Tabs
onChange={onChange}
destroyInactiveTabPane
activeKey={currentRoute?.key || activeKey}
defaultActiveKey={currentRoute?.key || activeKey}
animated
items={items}
tabBarExtraContent={
showRightSection && (
<HeaderRightSection
enableAnnouncements={false}
enableShare
enableFeedback
/>
)
}
{...rest}
/>
<TabsRoot
value={resolvedActiveKey}
defaultValue={defaultActiveKey ?? resolvedActiveKey}
onValueChange={onChange}
>
{!hideTabBar && (
<div className="route-tab-header">
<TabsList>
{routes.map(({ name, key }) => (
<TabsTrigger key={key} value={key}>
{name}
</TabsTrigger>
))}
</TabsList>
{extraContent && <div className="route-tab-extra">{extraContent}</div>}
</div>
)}
{routes.map(({ key, Component }) => (
<TabsContent key={key} value={key}>
<Component />
</TabsContent>
))}
</TabsRoot>
);
}
RouteTab.defaultProps = {
onChangeHandler: undefined,
showRightSection: true,
};
export default RouteTab;

View File

@@ -1,5 +1,5 @@
import { TabsProps } from 'antd';
import { History } from 'history';
import { ReactNode } from 'react';
export type TabRoutes = {
name: React.ReactNode;
@@ -10,8 +10,11 @@ export type TabRoutes = {
export interface RouteTabProps {
routes: TabRoutes[];
activeKey: TabsProps['activeKey'];
activeKey: string | undefined;
defaultActiveKey?: string;
onChangeHandler?: (key: string) => void;
history: History<unknown>;
showRightSection: boolean;
showRightSection?: boolean;
tabBarExtraContent?: ReactNode;
hideTabBar?: boolean;
}

View File

@@ -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 { CalendarClock, GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
import { GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef } from 'types/api/alerts/def';
@@ -172,24 +172,14 @@ function CreateRules(): JSX.Element {
</div>
),
},
{
label: (
<div className="periscope-tab top-level-tab">
<CalendarClock size={14} />
Planned Downtime
</div>
),
key: AlertListTabs.PLANNED_DOWNTIME,
children: null,
},
{
label: (
<div className="periscope-tab top-level-tab">
<ConfigureIcon width={14} height={14} />
Routing Policies
Configuration
</div>
),
key: AlertListTabs.ROUTING_POLICIES,
key: AlertListTabs.CONFIGURATION,
children: null,
},
];

View File

@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react';
import { Button, Input, Select, Tooltip } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Select, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { CircleX, Trash } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';

View File

@@ -4,4 +4,5 @@ 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=RoutingPolicies';
export const ROUTING_POLICIES_ROUTE =
'/alerts?tab=Configuration&subTab=routing-policies';

View File

@@ -1,4 +1,5 @@
import { Collapse, Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Collapse } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { useCreateAlertState } from '../context';

View File

@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { Input, Select } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Select } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { ADVANCED_OPTIONS_TIME_UNIT_OPTIONS } from '../../context/constants';

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import './TimeInput.scss';
export interface TimeInputProps {

View File

@@ -1,4 +1,5 @@
import { Input, Select } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Select } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { useCreateAlertState } from '../context';

View File

@@ -16,9 +16,10 @@ import {
Plus,
X,
} from '@signozhq/icons';
import { Button, Card, Input, Modal, Popover, Tooltip } from 'antd';
import { Button, Card, Modal, Popover, Tooltip } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { Typography } from '@signozhq/ui/typography';
import { Input } from '@signozhq/ui/input';
import logEvent from 'api/common/logEvent';
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';

View File

@@ -1,70 +0,0 @@
.settings-tabs {
.ant-tabs-nav-list {
height: 32px;
flex-shrink: 0;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
transition: opacity 0.1s !important;
.ant-tabs-tab + .ant-tabs-tab {
margin: 0px;
}
.ant-tabs-tab:not(:last-child) {
border-right: 1px solid var(--l1-border) !important;
}
.overview-btn {
width: 114px;
display: flex;
align-items: center;
justify-content: center;
}
.variables-btn {
width: 114px;
display: flex;
align-items: center;
justify-content: center;
}
.public-dashboard-btn {
width: 150px;
display: flex;
align-items: center;
justify-content: center;
&.disabled-btn {
opacity: 0.5;
cursor: not-allowed;
}
}
.ant-tabs-ink-bar {
display: none;
}
.ant-tabs-tab-active {
.overview-btn {
border-radius: 2px 0px 0px 2px;
background: var(--l1-border);
}
.variables-btn {
border-radius: 2px 0px 0px 2px;
background: var(--l1-border);
}
.public-dashboard-btn {
border-radius: 2px 0px 0px 2px;
background: var(--l1-border);
}
}
}
.ant-tabs-nav::before {
border-bottom: none;
}
}

View File

@@ -1,4 +1,4 @@
import { Button, Tabs, Tooltip } from 'antd';
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { Braces, Globe, Table } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
@@ -9,8 +9,6 @@ import DashboardVariableSettings from './DashboardVariableSettings';
import GeneralDashboardSettings from './General';
import PublicDashboardSetting from './PublicDashboard';
import './DashboardSettingsContent.styles.scss';
function DashboardSettings({
variablesSettingsTabHandle,
}: {
@@ -21,49 +19,26 @@ function DashboardSettings({
const enablePublicDashboard = isCloudUser || isEnterpriseSelfHostedUser;
const publicDashboardItem = {
label: (
<Tooltip
title={
user?.role !== USER_ROLES.ADMIN
? 'Only admins can publish / manage public dashboards'
: ''
}
placement="right"
>
<Button
type="text"
icon={<Globe size={14} />}
className={`public-dashboard-btn ${
user?.role !== USER_ROLES.ADMIN ? 'disabled-btn' : ''
}`}
>
Publish
</Button>
</Tooltip>
),
const publicDashboardItem: TabItemProps = {
key: 'public-dashboard',
label: 'Publish',
prefixIcon: <Globe size={14} />,
children: <PublicDashboardSetting />,
disabled: user?.role !== USER_ROLES.ADMIN,
disabledReason: 'Only admins can publish / manage public dashboards',
};
const items = [
const items: TabItemProps[] = [
{
label: (
<Button type="text" icon={<Table size={14} />} className="overview-btn">
Overview
</Button>
),
key: 'general',
label: 'Overview',
prefixIcon: <Table size={14} />,
children: <GeneralDashboardSettings />,
},
{
label: (
<Button type="text" icon={<Braces size={14} />} className="variables-btn">
Variables
</Button>
),
key: 'variables',
label: 'Variables',
prefixIcon: <Braces size={14} />,
children: (
<DashboardVariableSettings
variablesSettingsTabHandle={variablesSettingsTabHandle}
@@ -73,7 +48,7 @@ function DashboardSettings({
...(enablePublicDashboard ? [publicDashboardItem] : []),
];
return <Tabs items={items} animated className="settings-tabs" />;
return <Tabs items={items} />;
}
export default DashboardSettings;

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Button, Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button } from 'antd';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import { ResizeTable } from 'components/ResizeTable';
import { useNotifications } from 'hooks/useNotifications';

View File

@@ -22,7 +22,6 @@ import { Color } from '@signozhq/design-tokens';
import {
Button,
ColorPicker,
Input,
Modal,
RefSelectProps,
Select,
@@ -30,6 +29,7 @@ import {
} from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import { Input } from '@signozhq/ui/input';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';

View File

@@ -1,7 +1,7 @@
import { Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { Form, Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Form } from 'antd';
import { EmailChannel } from '../../CreateAlertChannels/config';
function EmailForm({ setSelectedConfig }: EmailFormProps): JSX.Element {

View File

@@ -1,6 +1,7 @@
import { Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { Form, Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Form } from 'antd';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import { WebhookChannel } from '../../CreateAlertChannels/config';

View File

@@ -1,7 +1,8 @@
import { Dispatch, ReactElement, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { Form, FormInstance, Input, Select } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Switch } from '@signozhq/ui/switch';
import { Form, FormInstance, Select } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import type { Store } from 'antd/lib/form/interface';
import ROUTES from 'constants/routes';

View File

@@ -6,7 +6,8 @@ import { useIsFetching } from 'react-query';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { Color } from '@signozhq/design-tokens';
import { Button, Form, Input, Modal } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Form, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';

View File

@@ -5,12 +5,12 @@ import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import {
Col,
Collapse,
DatePicker,
Form,
Input,
InputNumber,
Modal,
Row,

View File

@@ -1,4 +1,5 @@
import { Form, Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Form } from 'antd';
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
function RenderConnectionFields({

View File

@@ -1,5 +1,4 @@
import { Button, Tabs, TabsProps } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
import { CableCar, Group } from '@signozhq/icons';
import { IntegrationDetailedProps } from 'types/api/integrations/types';
@@ -22,18 +21,11 @@ function IntegrationDetailContent(
): JSX.Element {
const { activeDetailTab, integrationData, integrationId, setActiveDetailTab } =
props;
const items: TabsProps['items'] = [
const items: TabItemProps[] = [
{
key: 'overview',
label: (
<Button
type="text"
className="integration-tab-btns"
icon={<CableCar size={14} />}
>
<Typography.Text className="typography">Overview</Typography.Text>
</Button>
),
label: 'Overview',
prefixIcon: <CableCar size={14} />,
children: (
<Overview
categories={integrationData.categories}
@@ -44,15 +36,8 @@ function IntegrationDetailContent(
},
{
key: 'configuration',
label: (
<Button
type="text"
className="integration-tab-btns"
icon={<ConfigureIcon />}
>
<Typography.Text className="typography">Configure</Typography.Text>
</Button>
),
label: 'Configure',
prefixIcon: <ConfigureIcon />,
children: (
<Configure
configuration={integrationData.configuration}
@@ -62,15 +47,8 @@ function IntegrationDetailContent(
},
{
key: 'dataCollected',
label: (
<Button
type="text"
className="integration-tab-btns"
icon={<Group size={14} />}
>
<Typography.Text className="typography">Data Collected</Typography.Text>
</Button>
),
label: 'Data Collected',
prefixIcon: <Group size={14} />,
children: (
<DataCollected
logsData={integrationData.data_collected.logs}
@@ -81,11 +59,7 @@ function IntegrationDetailContent(
];
return (
<div className="integration-detail-container">
<Tabs
activeKey={activeDetailTab}
items={items}
onChange={setActiveDetailTab}
/>
<Tabs value={activeDetailTab} items={items} onChange={setActiveDetailTab} />
</div>
);
}

View File

@@ -168,45 +168,6 @@
padding: 10px 16px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
.integration-tab-btns {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 8px 18px 8px !important;
.typography {
color: var(--l1-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.integration-tab-btns:hover {
&.ant-btn-text {
background-color: unset !important;
}
}
.ant-tabs-nav-list {
gap: 24px;
}
.ant-tabs-nav {
padding: 0px !important;
}
.ant-tabs-tab {
padding: 0 !important;
}
.ant-tabs-tab + .ant-tabs-tab {
margin: 0px !important;
}
}
.uninstall-integration-bar {

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Form, Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Form } from 'antd';
import apply from 'api/v3/licenses/post';
import { useNotifications } from 'hooks/useNotifications';
import APIError from 'types/api/error';

View File

@@ -43,7 +43,6 @@ import { isModifierKeyPressed } from 'utils/app';
import DeleteAlert from './DeleteAlert';
import { ColumnButton, SearchContainer } from './styles';
import MutedBadge from './TableComponents/MutedBadge';
import Status from './TableComponents/Status';
import ToggleAlertState from './ToggleAlertState';
import { alertActionLogEvent, filterAlerts } from './utils';
@@ -277,14 +276,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
onEditHandler(record, { newTab: isModifierKeyPressed(e) });
};
const isMuted = Boolean(record.mutes?.length);
return (
<span className="alert-list-name-cell">
<Typography.Link onClick={onClickHandler}>{value}</Typography.Link>
{isMuted && <MutedBadge muteEndTime={record.mutes![0].end} />}
</span>
);
return <Typography.Link onClick={onClickHandler}>{value}</Typography.Link>;
},
sortOrder: sortedInfo.columnKey === 'name' ? sortedInfo.order : null,
},

View File

@@ -1,20 +0,0 @@
.alert-list-name-cell {
display: inline-flex;
align-items: center;
gap: 8px;
}
.alert-list-muted-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 7px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--bg-amber-500);
background: rgba(255, 205, 86, 0.12);
border: 1px solid rgba(255, 205, 86, 0.25);
border-radius: 4px;
}

View File

@@ -1,43 +0,0 @@
import { BellOff } from '@signozhq/icons';
import dayjs from 'dayjs';
import './MutedBadge.styles.scss';
const formatRemaining = (endTime: string | undefined | null): string | null => {
if (!endTime) {
return null;
}
const end = dayjs(endTime);
const now = dayjs();
const diffMs = end.diff(now);
if (diffMs <= 0) {
return null;
}
const totalMinutes = Math.floor(diffMs / 60000);
const days = Math.floor(totalMinutes / (60 * 24));
const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
const minutes = totalMinutes % 60;
if (days > 0) {
return `${days}d ${hours}h`;
}
if (hours > 0) {
return `${hours}h ${minutes}m`;
}
return `${minutes}m`;
};
interface MutedBadgeProps {
muteEndTime: string | undefined | null;
}
function MutedBadge({ muteEndTime }: MutedBadgeProps): JSX.Element | null {
const remaining = formatRemaining(muteEndTime);
return (
<span className="alert-list-muted-badge">
<BellOff size={10} />
<span>MUTED{remaining ? ` · ${remaining}` : ''}</span>
</span>
);
}
export default MutedBadge;

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { ChangeEvent, useState } from 'react';
import { Button, Input, Modal } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import ApacheIcon from 'assets/CustomIcons/ApacheIcon';
import DockerIcon from 'assets/CustomIcons/DockerIcon';

View File

@@ -12,17 +12,9 @@ import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import { Input } from '@signozhq/ui/input';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import {
Button,
Flex,
Input,
Modal,
Popover,
Skeleton,
Table,
Tooltip,
} from 'antd';
import { Button, Flex, Modal, Popover, Skeleton, Table, Tooltip } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';

View File

@@ -1,7 +1,8 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LoaderCircle, Check } from '@signozhq/icons';
import { Button, Input, Space } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { useNotifications } from 'hooks/useNotifications';

View File

@@ -2,8 +2,9 @@ import { ReactNode, useState } from 'react';
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { Switch } from '@signozhq/ui/switch';
import { Collapse, Input } from 'antd';
import { Collapse } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { Badge } from '@signozhq/ui/badge';
import { Typography } from '@signozhq/ui/typography';

View File

@@ -2,7 +2,8 @@ import { ChangeEvent, useCallback, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { CirclePlus, X } from '@signozhq/icons';
import { Col, Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Col } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import { AppState } from 'store/reducers';

View File

@@ -2,7 +2,8 @@ import { useCallback, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { SquareX, X } from '@signozhq/icons';
import { Button, Input, Select } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Select } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
import {
ConditionalOperators,

View File

@@ -1,6 +1,7 @@
import { Input } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Select } from 'antd';
// TODO(@signozhq/ui-input): migrate this <Input> once @signozhq/ui Input
// supports the `onWheel` handler (used to blur on scroll for number inputs).
import { Input, Select } from 'antd';
import classNames from 'classnames';
import { TIME_AGGREGATION_OPTIONS } from './constants';

View File

@@ -1,7 +1,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import type { TableColumnsType as ColumnsType } from 'antd';
import { Button, Collapse, Input, Select, Spin } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Collapse, Select, Spin } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import {

View File

@@ -7,7 +7,8 @@ import {
DropResult,
} from 'react-beautiful-dnd';
import { Color } from '@signozhq/design-tokens';
import { Button, Input, Tooltip } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Tooltip } from 'antd';
import {
DropdownMenu,
DropdownMenuContent,

View File

@@ -1,5 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { Button, Col, Form, Input as AntInput, Input, Row } from 'antd';
// TODO(@signozhq/ui-input): migrate <Input> once @signozhq/ui Input
// supports the `spellCheck` prop on the URL input below.
import { Button, Col, Form, Input, Input as AntInput, Row } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { CONTEXT_LINK_FIELDS } from 'container/NewWidget/RightContainer/ContextLinks/constants';
import {

View File

@@ -1,7 +1,8 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Blocks, Check, LoaderCircle } from '@signozhq/icons';
import { Button, Card, Form, Input, Select, Space } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Card, Form, Select, Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';

View File

@@ -1,7 +1,8 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Check, Server, LoaderCircle } from '@signozhq/icons';
import { Button, Card, Form, Input, Space } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Card, Form, Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';

View File

@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next';
import { Plus, Trash2 } from '@signozhq/icons';
import { Button, Form, FormInstance, Input, Select, Space } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Form, FormInstance, Select, Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';

View File

@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Form, Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Form } from 'antd';
import { ProcessorFormField } from '../../AddNewProcessor/config';
import { formValidationRules } from '../../config';

View File

@@ -1,4 +1,6 @@
import { ChangeEventHandler, useState } from 'react';
// TODO(@signozhq/ui-input): migrate to @signozhq/ui Input once the antd
// `InputProps` spread (`size`, etc.) is no longer needed on this wrapper.
import { Input, InputProps } from 'antd';
function CSVInput({ value, onChange, ...otherProps }: InputProps): JSX.Element {

View File

@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react';
import { Info } from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { Switch } from '@signozhq/ui/switch';
import { Flex, Form, Input, Space, Tooltip } from 'antd';
import { Flex, Form, Space, Tooltip } from 'antd';
import { ProcessorData } from 'types/api/pipeline/def';
import { PREDEFINED_MAPPING } from '../config';

View File

@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next';
import { Form, Input, Select, Space } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Switch } from '@signozhq/ui/switch';
import { Form, Select, Space } from 'antd';
import { ModalFooterTitle } from 'container/PipelinePage/styles';
import { ProcessorData } from 'types/api/pipeline/def';

View File

@@ -2,7 +2,8 @@ import React, { ChangeEvent, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Plus, Search } from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Flex, Form, Input, Tooltip } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Flex, Form, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import {
useDeleteDowntimeScheduleByID,

View File

@@ -152,6 +152,11 @@ export function PlannedDowntimeForm(
const saveHandler = useCallback(
async (values: PlannedDowntimeFormData) => {
const { startTime, timezone } = values;
if (!startTime || !timezone) {
// unreachable
return;
}
const data: AlertmanagertypesPostablePlannedMaintenanceDTO = {
alertIds:
values.alertRuleScope === 'all'
@@ -162,9 +167,9 @@ export function PlannedDowntimeForm(
name: values.name,
scope: values.scope,
schedule: {
startTime: values.startTime?.format(),
startTime: startTime.format(),
endTime: values.endTime?.format(),
timezone: values.timezone!,
timezone,
recurrence: values.recurrence,
},
};
@@ -201,25 +206,17 @@ export function PlannedDowntimeForm(
],
);
const onFinish = async (values: PlannedDowntimeFormData): Promise<void> => {
const { recurrence } = values;
const recurrenceData =
!recurrence ||
recurrence.repeatType === recurrenceOptions.doesNotRepeat.value
? undefined
: {
duration: recurrence.duration
? `${recurrence.duration}${durationUnit}`
: '',
startTime: values.startTime!.format(),
endTime: values.endTime?.format(),
repeatOn: recurrence.repeatOn,
repeatType: recurrence.repeatType,
};
const rec = values.recurrence;
const recurrence =
rec && rec.repeatType !== recurrenceOptions.doesNotRepeat.value
? {
duration: `${rec.duration}${durationUnit}`,
repeatOn: rec.repeatOn,
repeatType: rec.repeatType,
}
: undefined;
await saveHandler({
...values,
recurrence: recurrenceData,
});
await saveHandler({ ...values, recurrence });
};
const handleFormData = (data: Partial<PlannedDowntimeFormData>): void => {
@@ -276,9 +273,6 @@ export function PlannedDowntimeForm(
const formattedInitialValues = useMemo((): PlannedDowntimeFormData => {
const { schedule } = initialValues;
const startTime = schedule?.recurrence?.startTime || schedule?.startTime;
const endTime = schedule?.recurrence?.endTime || schedule?.endTime;
const initialAlertIds = initialValues.alertIds || [];
return {
@@ -286,8 +280,12 @@ export function PlannedDowntimeForm(
alertRuleScope:
isEditMode && initialAlertIds.length === 0 ? 'all' : 'specific',
alertRules: getAlertOptionsFromIds(initialAlertIds, alertOptions),
startTime: startTime ? dayjs(startTime).tz(schedule.timezone) : null,
endTime: endTime ? dayjs(endTime).tz(schedule.timezone) : null,
startTime: schedule?.startTime
? dayjs(schedule.startTime).tz(schedule.timezone)
: null,
endTime: schedule?.endTime
? dayjs(schedule.endTime).tz(schedule.timezone)
: null,
recurrence: {
...schedule?.recurrence,
repeatType: !isScheduleRecurring(schedule)
@@ -445,6 +443,12 @@ export function PlannedDowntimeForm(
<Form.Item
label="Ends on"
name="endTime"
required={recurrenceType === recurrenceOptions.doesNotRepeat.value}
rules={[
{
required: recurrenceType === recurrenceOptions.doesNotRepeat.value,
},
]}
className={!isEmpty(endTimeText) ? 'formItemWithBullet' : ''}
>
<DatePicker

View File

@@ -1,16 +1,15 @@
import React, { ReactNode, useEffect } from 'react';
import { UseQueryResult } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Badge, BadgeColor } from '@signozhq/ui/badge';
import { Typography } from '@signozhq/ui/typography';
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 {
AlertmanagertypesMaintenanceStatusDTO,
type ListDowntimeSchedules200,
type RenderErrorResponseDTO,
type AlertmanagertypesPlannedMaintenanceDTO,
type AlertmanagertypesScheduleDTO,
import type {
ListDowntimeSchedules200,
RenderErrorResponseDTO,
AlertmanagertypesPlannedMaintenanceDTO,
AlertmanagertypesScheduleDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { ErrorType } from 'api/generatedAPIInstance';
import cx from 'classnames';
@@ -33,50 +32,6 @@ import './PlannedDowntime.styles.scss';
const { Panel } = Collapse;
const STATUS_BADGE_PROPS: Record<
AlertmanagertypesMaintenanceStatusDTO,
{ color: BadgeColor; 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;
@@ -128,13 +83,11 @@ 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 {
@@ -142,10 +95,9 @@ function HeaderComponent({
const isCrudEnabled = user?.role !== USER_ROLES.VIEWER;
return (
<Flex className="header-content" justify="space-between">
<Flex gap={8} align="center">
<Flex gap={8}>
<Typography>{name}</Typography>
<Badge color="vanilla">{duration}</Badge>
<StatusBadge status={status} />
</Flex>
{isCrudEnabled && (
@@ -190,7 +142,6 @@ export function CollapseListContent({
updated_by_name?: string;
alertOptions?: DefaultOptionType[];
}): JSX.Element {
const repeats = schedule?.recurrence;
const renderItems = (title: string, value: ReactNode): JSX.Element => (
<div className="render-item-collapse-list">
<Typography>{title}</Typography>
@@ -232,9 +183,7 @@ export function CollapseListContent({
{renderItems(
'Timeframe',
schedule?.startTime ? (
<Typography>
{schedule?.endTime ? `${startTime}${endTime}` : `${startTime} onwards`}
</Typography>
<Typography>{`${startTime}${endTime}`}</Typography>
) : (
'-'
),
@@ -243,10 +192,7 @@ export function CollapseListContent({
'Timezone',
<Typography>{schedule?.timezone || '-'}</Typography>,
)}
{renderItems(
'Repeats',
<Typography>{recurrenceInfo(repeats, schedule?.timezone)}</Typography>,
)}
{renderItems('Repeats', <Typography>{recurrenceInfo(schedule)}</Typography>)}
{renderItems(
'Alerts silenced',
alertOptions?.length ? (
@@ -279,7 +225,6 @@ export function CustomCollapseList(
createdAt,
createdBy,
schedule,
status,
updatedAt,
updatedBy,
name,
@@ -308,7 +253,6 @@ export function CustomCollapseList(
: getDuration(schedule?.startTime || '', schedule?.endTime || '')
}
name={defaultTo(name, '')}
status={status}
handleEdit={() => {
setInitialValues({ ...props });
setModalOpen(true);
@@ -382,11 +326,6 @@ 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));
}

View File

@@ -6,7 +6,7 @@ import type {
DeleteDowntimeScheduleByIDPathParameters,
RenderErrorResponseDTO,
AlertmanagertypesPlannedMaintenanceDTO,
AlertmanagertypesRecurrenceDTO,
AlertmanagertypesScheduleDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { ErrorType } from 'api/generatedAPIInstance';
import { AxiosError } from 'axios';
@@ -66,14 +66,17 @@ export const getAlertOptionsFromIds = (
);
export const recurrenceInfo = (
recurrence?: AlertmanagertypesRecurrenceDTO | null,
timezone?: string,
schedule?: AlertmanagertypesScheduleDTO | null,
): string => {
if (!schedule) {
return 'No';
}
const { startTime, endTime, timezone, recurrence } = schedule;
if (!recurrence) {
return 'No';
}
const { startTime, duration, repeatOn, repeatType, endTime } = recurrence;
const { duration, repeatOn, repeatType } = recurrence;
const formattedStartTime = startTime
? formatDateTime(startTime, timezone)
@@ -95,7 +98,7 @@ export const defaultInitialValues: Partial<AlertmanagertypesPlannedMaintenanceDT
timezone: '',
endTime: undefined,
recurrence: undefined,
startTime: undefined,
startTime: '',
},
alertIds: [],
createdAt: undefined,

View File

@@ -11,7 +11,7 @@ export const buildSchedule = (
schedule: Partial<AlertmanagertypesScheduleDTO>,
): AlertmanagertypesScheduleDTO => ({
timezone: schedule?.timezone ?? '',
startTime: schedule?.startTime,
startTime: schedule?.startTime ?? '',
endTime: schedule?.endTime,
recurrence: schedule?.recurrence,
});

View File

@@ -8,4 +8,16 @@
grid-template-columns: 60% 35%;
justify-content: space-between;
gap: 16px;
.input-with-label {
.label {
box-sizing: border-box;
height: 36px;
}
.input {
box-sizing: border-box;
height: 36px;
}
}
}

View File

@@ -1,6 +1,7 @@
import { useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { Input, Skeleton } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Skeleton } from 'antd';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { QUERY_BUILDER_KEY_TYPES } from 'constants/antlrQueryConstants';

View File

@@ -1,7 +1,8 @@
import { ChangeEvent, useMemo } from 'react';
import { Plus, Search } from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Flex, Input, Tooltip } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Flex, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';

View File

@@ -1,5 +1,5 @@
import { useCallback, useMemo, useState } from 'react';
import { Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';

View File

@@ -1,5 +1,6 @@
import { useState } from 'react';
import { Collapse, Input, Modal } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Collapse, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { Diamond } from '@signozhq/icons';

View File

@@ -9,10 +9,10 @@ import {
useState,
} from 'react';
import { useMutation, useQuery } from 'react-query';
import { Input } from '@signozhq/ui/input';
import {
Button,
Checkbox,
Input,
Modal,
Select,
Skeleton,

View File

@@ -1,5 +1,7 @@
import { Input } from 'antd';
// TODO(@signozhq/ui-input): migrate this styled(Input) once @signozhq/ui
// Input supports `addonAfter` (the consumer renders `<InputComponent addonAfter="ms">`).
import { Typography } from '@signozhq/ui/typography';
import { Input } from 'antd';
import styled from 'styled-components';
export const DurationText = styled.div`

View File

@@ -8,7 +8,8 @@ import {
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { AutoComplete, Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import { AutoComplete } from 'antd';
import getTagFilters from 'api/trace/getTagFilter';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';

View File

@@ -2,7 +2,8 @@ import { useMemo, useState } from 'react';
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { AutoComplete, Input, Space } from 'antd';
import { Input } from '@signozhq/ui/input';
import { AutoComplete, Space } from 'antd';
import getTagFilters from 'api/trace/getTagFilter';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';

View File

@@ -1,6 +1,7 @@
import { ChangeEvent, useEffect, useMemo, useState } from 'react';
import { ArrowLeft, Check, Loader, Plus, Search } from '@signozhq/icons';
import { Button, Input, Spin } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Spin } from 'antd';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import SignozModal from 'components/SignozModal/SignozModal';

View File

@@ -1,4 +1,4 @@
import { Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import styled from 'styled-components';
export const InputComponent = styled(Input)`

View File

@@ -13,20 +13,6 @@
align-items: center;
}
}
.alert-state-segmented-wrapper {
position: relative;
display: inline-flex;
}
.alert-state-segmented-anchor {
position: absolute;
right: 0;
bottom: 0;
width: 0;
height: 0;
pointer-events: none;
}
.dropdown-menu {
border-radius: 4px;
box-shadow: none;

View File

@@ -1,8 +1,9 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Button, Tooltip } from 'antd';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import { Divider } from '@signozhq/ui/divider';
import { Switch } from '@signozhq/ui/switch';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Copy, Ellipsis, PenLine, Trash2 } from '@signozhq/icons';
import {
@@ -17,13 +18,6 @@ import { NEW_ALERT_SCHEMA_VERSION } from 'types/api/alerts/alertTypesV2';
import { AlertDef } from 'types/api/alerts/def';
import { AlertHeaderProps } from '../AlertHeader';
import AlertStateSegmented, {
AlertSegmentedState,
} from '../MuteAlert/AlertStateSegmented';
import MutePopover from '../MuteAlert/MutePopover';
import MuteSchedulerDrawer from '../MuteAlert/MuteSchedulerDrawer';
import { useActiveMutes } from '../MuteAlert/useActiveMutes';
import { useMuteAlertRule } from '../MuteAlert/useMuteAlertRule';
import RenameModal from './RenameModal';
import './ActionButtons.styles.scss';
@@ -113,77 +107,19 @@ function AlertActionButtons({
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => (): void => setAlertRuleState(undefined), []);
const { activeMutes, refetch: refetchActiveMute } = useActiveMutes(ruleId);
const segmentedState: AlertSegmentedState = useMemo(() => {
if (isAlertRuleDisabled) {
return 'disabled';
}
if (activeMutes.length) {
return 'muted';
}
return 'active';
}, [isAlertRuleDisabled, activeMutes]);
const [isMutePopoverOpen, setIsMutePopoverOpen] = useState<boolean>(false);
const [isMuteDrawerOpen, setIsMuteDrawerOpen] = useState<boolean>(false);
const { mute, isLoading: isMuting } = useMuteAlertRule({
ruleId,
onSuccess: () => {
setIsMutePopoverOpen(false);
setIsMuteDrawerOpen(false);
refetchActiveMute();
},
});
const handleActiveClick = useCallback(() => {
// If currently disabled, re-enable. Otherwise (already active) no-op.
// When muted, the segmented control disables this button.
if (isAlertRuleDisabled) {
setIsAlertRuleDisabled(false);
handleAlertStateToggle();
}
}, [isAlertRuleDisabled, handleAlertStateToggle]);
const handleMuteClick = useCallback(() => {
if (segmentedState === 'active') {
setIsMutePopoverOpen(true);
}
}, [segmentedState]);
const handleDisableClick = useCallback(() => {
if (!isAlertRuleDisabled) {
setIsAlertRuleDisabled(true);
handleAlertStateToggle();
}
}, [isAlertRuleDisabled, handleAlertStateToggle]);
const ruleDisplayName = alertRuleName ?? alertDetails.alert;
const toggleAlertRule = useCallback(() => {
setIsAlertRuleDisabled((prev) => !prev);
handleAlertStateToggle();
}, [handleAlertStateToggle]);
return (
<>
<div className="alert-action-buttons">
{isAlertRuleDisabled !== undefined && (
<div className="alert-state-segmented-wrapper">
<AlertStateSegmented
state={segmentedState}
onActive={handleActiveClick}
onMute={handleMuteClick}
onDisable={handleDisableClick}
/>
<MutePopover
open={isMutePopoverOpen}
onOpenChange={setIsMutePopoverOpen}
ruleName={ruleDisplayName}
isLoading={isMuting}
onSubmit={mute}
onOpenCustomWindow={(): void => setIsMuteDrawerOpen(true)}
anchor={<span className="alert-state-segmented-anchor" />}
/>
</div>
)}
<Tooltip title={isAlertRuleDisabled ? 'Enable alert' : 'Disable alert'}>
{isAlertRuleDisabled !== undefined && (
<Switch onChange={toggleAlertRule} value={!isAlertRuleDisabled} />
)}
</Tooltip>
<CopyToClipboard textToCopy={window.location.href} />
<Divider type="vertical" className="alert-action-buttons__divider" />
@@ -205,14 +141,6 @@ function AlertActionButtons({
</DropdownMenuSimple>
</div>
<MuteSchedulerDrawer
open={isMuteDrawerOpen}
onClose={(): void => setIsMuteDrawerOpen(false)}
ruleName={ruleDisplayName}
isLoading={isMuting}
onSubmit={mute}
/>
<RenameModal
isOpen={isRenameAlertOpen}
setIsOpen={setIsRenameAlertOpen}

View File

@@ -1,13 +1,3 @@
.alert-info-wrapper {
display: flex;
flex-direction: column;
gap: 4px;
}
.alert-info__banner {
padding: 0 16px;
}
.alert-info {
display: flex;
justify-content: space-between;

View File

@@ -12,9 +12,6 @@ import AlertActionButtons from './ActionButtons/ActionButtons';
import AlertLabels from './AlertLabels/AlertLabels';
import AlertSeverity from './AlertSeverity/AlertSeverity';
import AlertState from './AlertState/AlertState';
import DisabledBanner from './MuteAlert/DisabledBanner';
import MutedBanner from './MuteAlert/MutedBanner';
import { useActiveMutes } from './MuteAlert/useActiveMutes';
import './AlertHeader.styles.scss';
@@ -46,13 +43,6 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
const isV2Alert = alertDetails.schemaVersion === NEW_ALERT_SCHEMA_VERSION;
const ruleId = alertDetails?.id || '';
const { activeMutes, refetch } = useActiveMutes(ruleId);
const effectiveState = alertRuleState ?? state ?? '';
const isDisabled = effectiveState === 'disabled';
const showMutedBanner = !isDisabled && Boolean(activeMutes.length);
const showDisabledBanner = isDisabled;
const CreateAlertV1Header = (
<div className="alert-info__info-wrapper">
<div className="top-section">
@@ -77,23 +67,14 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
);
return (
<div className="alert-info-wrapper">
<div className="alert-info">
{isV2Alert ? <CreateAlertV2Header /> : CreateAlertV1Header}
<div className="alert-info__action-buttons">
<AlertActionButtons alertDetails={alertDetails} ruleId={ruleId} />
</div>
<div className="alert-info">
{isV2Alert ? <CreateAlertV2Header /> : CreateAlertV1Header}
<div className="alert-info__action-buttons">
<AlertActionButtons
alertDetails={alertDetails}
ruleId={alertDetails?.id || ''}
/>
</div>
{showMutedBanner && (
<div className="alert-info__banner">
<MutedBanner activeMute={activeMutes[0]} onExpire={refetch} />
</div>
)}
{showDisabledBanner && (
<div className="alert-info__banner">
<DisabledBanner rule={alertDetails as RuletypesRuleDTO} />
</div>
)}
</div>
);
}

View File

@@ -1,102 +0,0 @@
.alert-state-segmented {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 3px;
background: var(--bg-ink-300);
border: 1px solid var(--bg-slate-300);
border-radius: 999px;
&__pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
font-size: 12px;
font-weight: 500;
line-height: 1;
color: var(--bg-vanilla-400);
background: transparent;
border: 0;
border-radius: 999px;
cursor: pointer;
transition:
background 140ms,
color 140ms;
&:hover:not(:disabled) {
color: var(--bg-vanilla-100);
background: var(--bg-ink-200);
}
&:disabled {
cursor: not-allowed;
}
&:focus-visible {
outline: 2px solid var(--bg-robin-500);
outline-offset: 2px;
}
&--active-active {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
&:hover:not(:disabled) {
background: var(--bg-robin-600);
color: var(--bg-vanilla-100);
}
}
&.alert-state-segmented__pill--active-muted {
background: var(--bg-amber-500);
color: #1a1407;
.alert-state-segmented__icon,
.alert-state-segmented__label {
color: #1a1407;
}
&:hover:not(:disabled) {
background: var(--bg-amber-600);
}
}
&--active-disabled {
background: var(--bg-slate-100);
color: var(--bg-vanilla-100);
&:hover:not(:disabled) {
background: var(--bg-slate-200);
color: var(--bg-vanilla-100);
}
}
}
&__dot {
width: 6px;
height: 6px;
background: var(--bg-vanilla-100);
border-radius: 999px;
}
&__icon {
flex-shrink: 0;
}
}
.lightMode {
.alert-state-segmented {
background: var(--bg-vanilla-200);
border-color: var(--bg-slate-500);
&__pill {
color: var(--bg-ink-300);
&:hover:not(:disabled) {
color: var(--bg-ink-500);
background: var(--bg-vanilla-300);
}
}
}
}

View File

@@ -1,86 +0,0 @@
import { forwardRef } from 'react';
import { BellOff } from '@signozhq/icons';
import classNames from 'classnames';
import './AlertStateSegmented.styles.scss';
export type AlertSegmentedState = 'active' | 'muted' | 'disabled';
export interface AlertStateSegmentedProps {
state: AlertSegmentedState;
onActive: () => void;
onMute: () => void;
onDisable: () => void;
disabled?: boolean;
}
const AlertStateSegmented = forwardRef<
HTMLDivElement,
AlertStateSegmentedProps
>(function AlertStateSegmented(props, ref): JSX.Element {
const { state, onActive, onMute, onDisable, disabled } = props;
const isMuted = state === 'muted';
const isDisabled = state === 'disabled';
return (
<div
className="alert-state-segmented"
role="tablist"
aria-label="Alert rule state"
ref={ref}
>
<button
type="button"
role="tab"
aria-selected={state === 'active'}
aria-label="Active"
className={classNames('alert-state-segmented__pill', {
'alert-state-segmented__pill--active-active': state === 'active',
})}
onClick={onActive}
// Per spec: when muted, un-muting must happen via Planned Downtimes,
// so the Active pill is non-interactive while muted.
disabled={disabled || isMuted}
>
{state === 'active' && (
<span className="alert-state-segmented__dot" aria-hidden />
)}
<span className="alert-state-segmented__label">Active</span>
</button>
<button
type="button"
role="tab"
aria-selected={state === 'muted'}
aria-label="Mute"
className={classNames('alert-state-segmented__pill', {
'alert-state-segmented__pill--active-muted': state === 'muted',
})}
onClick={onMute}
// Muting a disabled rule wouldn't change observable behavior, so the
// Mute pill is non-interactive while disabled.
disabled={disabled || isDisabled}
>
{state === 'muted' && (
<BellOff size={12} className="alert-state-segmented__icon" />
)}
<span className="alert-state-segmented__label">Mute</span>
</button>
<button
type="button"
role="tab"
aria-selected={state === 'disabled'}
aria-label="Disable"
className={classNames('alert-state-segmented__pill', {
'alert-state-segmented__pill--active-disabled': state === 'disabled',
})}
onClick={onDisable}
disabled={disabled}
>
<span className="alert-state-segmented__label">Disable</span>
</button>
</div>
);
});
export default AlertStateSegmented;

View File

@@ -1,56 +0,0 @@
import { useEffect, useState } from 'react';
import { CircleOff } from '@signozhq/icons';
import type { RuletypesRuleDTO } from 'api/generated/services/sigNoz.schemas';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import './StateBanners.styles.scss';
dayjs.extend(relativeTime);
interface DisabledBannerProps {
rule: RuletypesRuleDTO;
}
function DisabledBanner({ rule }: DisabledBannerProps): JSX.Element {
const updatedAt = rule.updatedAt ? dayjs(rule.updatedAt) : null;
const [fromNow, setFromNow] = useState(updatedAt?.fromNow() ?? null);
useEffect(() => {
if (!rule.updatedAt) {
return;
}
const interval = setInterval(
() => setFromNow(dayjs(rule.updatedAt).fromNow()),
60_000,
);
return (): void => clearInterval(interval);
}, [rule.updatedAt]);
return (
<div className="state-banner state-banner--disabled" role="status">
<div className="state-banner__icon-disc state-banner__icon-disc--disabled">
<CircleOff size={18} color="var(--bg-slate-50)" />
</div>
<div className="state-banner__body">
<div className="state-banner__title">
<span>Rule disabled</span>
<span className="state-banner__pill state-banner__pill--disabled">
NOT EVALUATING
</span>
</div>
<div className="state-banner__meta">
<span>Evaluation paused no alerts will be recorded</span>
{fromNow && (
<>
{' · '}
<span>{fromNow}</span>
</>
)}
</div>
</div>
</div>
);
}
export default DisabledBanner;

View File

@@ -1,193 +0,0 @@
.mute-popover-overlay {
.ant-popover-inner {
padding: 0;
background: var(--bg-ink-400);
border: 1px solid var(--bg-slate-300);
border-radius: 10px;
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.55);
}
.ant-popover-inner-content {
padding: 0;
}
}
.mute-popover {
width: 320px;
padding: 14px;
font-family: 'Inter', sans-serif;
color: var(--bg-vanilla-100);
&__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
&__title {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
color: var(--bg-vanilla-100);
}
&__close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
padding: 0;
color: var(--bg-vanilla-400);
background: transparent;
border: 0;
border-radius: 4px;
cursor: pointer;
transition:
color 140ms,
background 140ms;
&:hover {
color: var(--bg-vanilla-100);
background: var(--bg-ink-300);
}
}
&__hint {
margin: 0 0 12px 0;
font-size: 12px;
line-height: 1.45;
color: var(--bg-vanilla-400);
strong {
color: var(--bg-vanilla-100);
font-weight: 600;
}
}
&__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
margin-bottom: 8px;
}
&__cell {
padding: 9px 0;
font-size: 12.5px;
color: var(--bg-vanilla-100);
background: var(--bg-ink-300);
border: 1px solid var(--bg-slate-300);
border-radius: 6px;
cursor: pointer;
transition:
background 140ms,
border-color 140ms,
color 140ms;
&:hover:not(&--selected) {
background: rgba(78, 116, 248, 0.08);
border-color: var(--bg-robin-500);
}
&--selected {
background: var(--bg-robin-500);
border-color: var(--bg-robin-500);
color: var(--bg-vanilla-100);
}
}
&__custom {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
width: 100%;
padding: 8px 12px;
font-size: 12.5px;
color: var(--bg-vanilla-100);
background: transparent;
border: 1px dashed var(--bg-slate-200);
border-radius: 6px;
cursor: pointer;
transition:
border-color 140ms,
background 140ms;
&:hover {
border-color: var(--bg-robin-500);
background: rgba(78, 116, 248, 0.06);
}
}
&__divider {
height: 1px;
margin: 12px 0;
background: var(--bg-slate-300);
}
&__label {
display: block;
margin-bottom: 6px;
font-size: 12px;
color: var(--bg-vanilla-400);
}
&__input.ant-input {
padding: 8px 10px;
font-size: 12.5px;
background: var(--bg-ink-300);
border: 1px solid var(--bg-slate-300);
border-radius: 6px;
color: var(--bg-vanilla-100);
}
&__footer {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 12px;
}
&__btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
font-size: 12.5px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition:
background 140ms,
color 140ms;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&--ghost {
color: var(--bg-vanilla-400);
background: transparent;
border: 1px solid transparent;
&:hover:not(:disabled) {
color: var(--bg-vanilla-100);
background: var(--bg-ink-300);
}
}
&--primary {
color: var(--bg-vanilla-100);
background: var(--bg-robin-500);
border: 1px solid var(--bg-robin-500);
&:hover:not(:disabled) {
background: var(--bg-robin-600);
}
}
}
}

View File

@@ -1,261 +0,0 @@
import { useEffect, useState } from 'react';
import { BellOff, Calendar, X } from '@signozhq/icons';
import { Input, Popover } from 'antd';
import classNames from 'classnames';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import type { MutePayload } from './useMuteAlertRule';
import './MutePopover.styles.scss';
dayjs.extend(utc);
dayjs.extend(timezone);
type QuickDuration = {
label: string;
value: string;
minutes: number | null; // null = forever
};
export const QUICK_DURATIONS: QuickDuration[] = [
{ label: '15 min', value: '15m', minutes: 15 },
{ label: '1 hour', value: '1h', minutes: 60 },
{ label: '4 hours', value: '4h', minutes: 240 },
{ label: '1 day', value: '1d', minutes: 60 * 24 },
{ label: '1 week', value: '1w', minutes: 60 * 24 * 7 },
{ label: 'Forever', value: 'forever', minutes: null },
];
const DEFAULT_DURATION_VALUE = '4h';
export const buildMutePayloadFromQuickDuration = (
durationValue: string,
name: string,
): MutePayload | null => {
const duration = QUICK_DURATIONS.find((d) => d.value === durationValue);
if (!duration) {
return null;
}
// Format the times in the selected timezone so the ISO offset matches the
// `timezone` field. The backend ignores the offset and re-attaches the
// timezone to the raw wall-clock time, so the two must agree (mirrors
// PlannedDowntimeForm).
const tz = dayjs.tz.guess?.() || 'UTC';
const now = dayjs();
const startTime = now.tz(tz).format();
// duration.minutes === null → "Forever"; send endTime as null so the
// backend treats the mute as indefinite.
const endTime =
duration.minutes === null
? null
: now.add(duration.minutes, 'minute').tz(tz).format();
return {
name,
startTime,
endTime,
timezone: tz,
};
};
const getDefaultMuteName = (ruleName: string | undefined): string =>
ruleName ? `Muted: ${ruleName}` : 'Muted alert';
interface MutePopoverProps {
open: boolean;
onOpenChange: (open: boolean) => void;
anchor: React.ReactNode;
ruleName: string | undefined;
isLoading: boolean;
onSubmit: (payload: MutePayload) => Promise<void> | void;
onOpenCustomWindow: () => void;
}
function MutePopover(props: MutePopoverProps): JSX.Element {
const {
open,
onOpenChange,
anchor,
ruleName,
isLoading,
onSubmit,
onOpenCustomWindow,
} = props;
const [selected, setSelected] = useState<string>(DEFAULT_DURATION_VALUE);
const [name, setName] = useState<string>(getDefaultMuteName(ruleName));
useEffect(() => {
if (open) {
setSelected(DEFAULT_DURATION_VALUE);
setName(getDefaultMuteName(ruleName));
}
}, [open, ruleName]);
// Close on outside click / Escape. We use trigger={[]} on the Popover so
// antd doesn't handle these — without this hook, the popover only closes
// via Cancel / × / Mute submit.
useEffect(() => {
if (!open) {
return undefined;
}
// Drop focus so the trigger button doesn't show a :focus-visible
// outline after the popover closes via Escape / outside click.
const closeAndBlur = (): void => {
(document.activeElement as HTMLElement | null)?.blur();
onOpenChange(false);
};
const handleMouseDown = (e: MouseEvent): void => {
const target = e.target as HTMLElement | null;
if (target?.closest('.mute-popover-overlay')) {
return;
}
closeAndBlur();
};
const handleKey = (e: KeyboardEvent): void => {
if (e.key === 'Escape') {
closeAndBlur();
}
};
// Defer attaching listeners until after the click that opened the
// popover has finished bubbling — otherwise it counts as an outside
// click and we close immediately.
const timer = window.setTimeout(() => {
document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('keydown', handleKey);
}, 0);
return (): void => {
window.clearTimeout(timer);
document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('keydown', handleKey);
};
}, [open, onOpenChange]);
const selectedDuration = QUICK_DURATIONS.find((d) => d.value === selected);
const primaryLabel =
selectedDuration?.minutes === null
? 'Mute indefinitely'
: `Mute for ${selectedDuration?.label.toLowerCase() ?? '4 hours'}`;
const handleSubmit = async (): Promise<void> => {
const payload = buildMutePayloadFromQuickDuration(selected, name.trim());
if (!payload || !payload.name) {
return;
}
await onSubmit(payload);
};
const content = (
<div
className="mute-popover"
onKeyDown={(e): void => {
if (e.key === 'Escape') {
onOpenChange(false);
}
}}
>
<div className="mute-popover__header">
<div className="mute-popover__title">
<BellOff size={14} />
<span>Mute notifications</span>
</div>
<button
type="button"
aria-label="Close"
className="mute-popover__close"
onClick={(): void => onOpenChange(false)}
>
<X size={14} />
</button>
</div>
<p className="mute-popover__hint">
Rule keeps evaluating in the background. You&apos;ll still see fires in{' '}
<strong>History</strong> just no pages, Slack, or email.
</p>
<div className="mute-popover__grid">
{QUICK_DURATIONS.map((d) => (
<button
type="button"
key={d.value}
className={classNames('mute-popover__cell', {
'mute-popover__cell--selected': selected === d.value,
})}
onClick={(): void => setSelected(d.value)}
>
{d.label}
</button>
))}
</div>
<button
type="button"
className="mute-popover__custom"
onClick={(): void => {
onOpenChange(false);
onOpenCustomWindow();
}}
>
<Calendar size={14} />
Custom window
</button>
<div className="mute-popover__divider" />
<label className="mute-popover__label" htmlFor="mute-popover-name">
Name
</label>
<Input
id="mute-popover-name"
className="mute-popover__input"
placeholder="e.g. Deployment window"
value={name}
onChange={(e): void => setName(e.target.value)}
maxLength={120}
/>
<div className="mute-popover__footer">
<button
type="button"
className="mute-popover__btn mute-popover__btn--ghost"
onClick={(): void => onOpenChange(false)}
disabled={isLoading}
>
Cancel
</button>
<button
type="button"
className="mute-popover__btn mute-popover__btn--primary"
onClick={handleSubmit}
disabled={isLoading || !name.trim()}
>
<BellOff size={12} />
{primaryLabel}
</button>
</div>
</div>
);
return (
<Popover
open={open}
onOpenChange={onOpenChange}
trigger={[]}
placement="bottomRight"
arrow={false}
destroyTooltipOnHide
overlayClassName="mute-popover-overlay"
content={content}
>
{anchor}
</Popover>
);
}
export default MutePopover;

View File

@@ -1,125 +0,0 @@
.mute-scheduler-drawer {
.ant-drawer-body {
padding: 24px 28px;
background: var(--bg-ink-500);
}
.ant-drawer-header {
display: none;
}
&__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
&__title {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: var(--bg-vanilla-100);
}
&__close {
position: absolute;
top: 18px;
right: 24px;
width: 28px;
height: 28px;
font-size: 18px;
line-height: 1;
color: var(--bg-vanilla-400);
background: transparent;
border: 0;
border-radius: 4px;
cursor: pointer;
transition:
background 140ms,
color 140ms;
&:hover {
color: var(--bg-vanilla-100);
background: var(--bg-ink-300);
}
}
&__subtitle {
margin: 8px 0 14px 0;
font-size: 12.5px;
line-height: 1.55;
color: var(--bg-vanilla-400);
strong {
color: var(--bg-vanilla-100);
font-weight: 600;
}
}
&__divider {
height: 1px;
margin: 0 0 16px 0;
background: var(--bg-slate-300);
}
&__form {
.ant-form-item-label > label {
font-size: 12px;
color: var(--bg-vanilla-400);
}
}
&__row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
&__date {
width: 100%;
}
&__callout {
display: flex;
align-items: flex-start;
gap: 8px;
margin: 4px 0 18px 0;
padding: 10px;
background: rgba(35, 196, 248, 0.06);
border: 1px solid rgba(35, 196, 248, 0.2);
border-radius: 6px;
p {
margin: 0;
font-size: 12px;
line-height: 1.5;
color: var(--bg-vanilla-400);
strong {
color: var(--bg-vanilla-100);
font-weight: 600;
}
}
}
&__footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 16px;
margin-top: 6px;
border-top: 1px solid var(--bg-slate-300);
}
}
// The DatePicker popup is portaled to <body>, so this selector lives at the
// root rather than nested under .mute-scheduler-drawer. Without a solid panel
// background the time-column cells are transparent and the drawer content
// bleeds through.
.mute-scheduler-drawer__date-popup {
.ant-picker-panel-container {
background: var(--bg-ink-400) !important;
}
}

View File

@@ -1,258 +0,0 @@
import { useEffect, useMemo, useState } from 'react';
import { BellOff, Check, Info } from '@signozhq/icons';
import { Button, DatePicker, Drawer, Form, Input, Select } from 'antd';
import type { DefaultOptionType } from 'antd/es/select';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import {
recurrenceOptions,
recurrenceOptionWithSubmenu,
recurrenceWeeklyOptions,
} from 'container/PlannedDowntime/PlannedDowntimeutils';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import { ALL_TIME_ZONES } from 'utils/timeZoneUtil';
import type { MutePayload } from './useMuteAlertRule';
import './MuteSchedulerDrawer.styles.scss';
dayjs.extend(utc);
dayjs.extend(timezone);
const DATE_FORMAT = DATE_TIME_FORMATS.ORDINAL_DATETIME;
const TZ_OPTIONS: DefaultOptionType[] = ALL_TIME_ZONES.map((tz) => ({
label: tz,
value: tz,
key: tz,
}));
const DURATION_UNIT_OPTIONS = [
{ label: 'Mins', value: 'm' },
{ label: 'Hours', value: 'h' },
];
type MuteSchedulerFormData = {
name: string;
startTime: dayjs.Dayjs | null;
endTime: dayjs.Dayjs | null;
repeatType: string;
repeatOn?: string[];
duration?: number;
timezone: string;
};
interface MuteSchedulerDrawerProps {
open: boolean;
onClose: () => void;
ruleName: string | undefined;
isLoading: boolean;
onSubmit: (payload: MutePayload) => Promise<void> | void;
}
function MuteSchedulerDrawer(props: MuteSchedulerDrawerProps): JSX.Element {
const { open, onClose, ruleName, isLoading, onSubmit } = props;
const [form] = Form.useForm<MuteSchedulerFormData>();
const [recurrenceType, setRecurrenceType] = useState<string>(
recurrenceOptions.doesNotRepeat.value,
);
const [durationUnit, setDurationUnit] = useState<string>('m');
const defaultName = useMemo(
() => (ruleName ? `Muted: ${ruleName}` : 'Muted alert'),
[ruleName],
);
useEffect(() => {
if (open) {
const guess = (dayjs as any).tz?.guess?.() || 'UTC';
form.setFieldsValue({
name: defaultName,
startTime: dayjs(),
endTime: dayjs().add(1, 'hour'),
repeatType: recurrenceOptions.doesNotRepeat.value,
timezone: guess,
});
setRecurrenceType(recurrenceOptions.doesNotRepeat.value);
setDurationUnit('m');
}
}, [open, defaultName, form]);
const handleFinish = async (values: MuteSchedulerFormData): Promise<void> => {
const isRecurring =
values.repeatType &&
values.repeatType !== recurrenceOptions.doesNotRepeat.value;
// Reinterpret the picked wall-clock time in the selected timezone (keep
// local time, swap the offset) so the formatted ISO offset matches the
// `timezone` field. The backend ignores the offset and re-attaches the
// timezone to the raw time, so the two must agree (mirrors
// PlannedDowntimeForm's handleFormData).
const { timezone: tz } = values;
const startTime = (values.startTime || dayjs()).tz(tz, true).format();
const endTime = values.endTime ? values.endTime.tz(tz, true).format() : null;
const payload: MutePayload = {
name: values.name.trim(),
startTime,
endTime,
timezone: tz,
recurrence: isRecurring
? {
duration: values.duration ? `${values.duration}${durationUnit}` : '',
repeatOn: values.repeatOn as any,
repeatType: values.repeatType as any,
startTime,
endTime: endTime ?? undefined,
}
: undefined,
};
await onSubmit(payload);
};
const requiredRule = [{ required: true }];
return (
<Drawer
width={460}
open={open}
onClose={onClose}
placement="right"
closable={false}
destroyOnClose
className="mute-scheduler-drawer"
rootClassName="mute-scheduler-drawer-root"
>
<div className="mute-scheduler-drawer__header">
<div className="mute-scheduler-drawer__title">
<BellOff size={18} color="var(--bg-amber-500)" />
<span>Mute this alert rule</span>
</div>
<button
type="button"
className="mute-scheduler-drawer__close"
aria-label="Close"
onClick={onClose}
>
×
</button>
</div>
<p className="mute-scheduler-drawer__subtitle">
Creates a planned silence for <strong>{ruleName || 'this rule'}</strong>
rule continues to evaluate; notifications are suppressed for the window
below.
</p>
<div className="mute-scheduler-drawer__divider" />
<Form<MuteSchedulerFormData>
form={form}
layout="vertical"
onFinish={handleFinish}
onValuesChange={(_, all): void => {
if (all.repeatType !== recurrenceType) {
setRecurrenceType(all.repeatType);
}
}}
className="mute-scheduler-drawer__form"
autoComplete="off"
>
<Form.Item label="Name" name="name" rules={requiredRule}>
<Input placeholder="e.g. Deployment window" maxLength={120} />
</Form.Item>
<Form.Item label="Starts" name="startTime" rules={requiredRule}>
<DatePicker
className="mute-scheduler-drawer__date"
popupClassName="mute-scheduler-drawer__date-popup"
showTime
showNow={false}
format={(date): string => date.format(DATE_FORMAT)}
/>
</Form.Item>
<Form.Item
label="Ends"
name="endTime"
required={recurrenceType === recurrenceOptions.doesNotRepeat.value}
rules={[
{
required: recurrenceType === recurrenceOptions.doesNotRepeat.value,
},
]}
>
<DatePicker
className="mute-scheduler-drawer__date"
popupClassName="mute-scheduler-drawer__date-popup"
showTime
showNow={false}
format={(date): string => date.format(DATE_FORMAT)}
/>
</Form.Item>
<div className="mute-scheduler-drawer__row">
<Form.Item label="Repeats every" name="repeatType" rules={requiredRule}>
<Select placeholder="Select" options={recurrenceOptionWithSubmenu} />
</Form.Item>
<Form.Item label="Timezone" name="timezone" rules={requiredRule}>
<Select placeholder="Select timezone" showSearch options={TZ_OPTIONS} />
</Form.Item>
</div>
{recurrenceType === recurrenceOptions.weekly.value && (
<Form.Item label="Weekly occurrence" name="repeatOn" rules={requiredRule}>
<Select
placeholder="Select days"
mode="multiple"
options={Object.values(recurrenceWeeklyOptions)}
/>
</Form.Item>
)}
{recurrenceType &&
recurrenceType !== recurrenceOptions.doesNotRepeat.value && (
<Form.Item label="Duration" name="duration" rules={requiredRule}>
<Input
type="number"
min={1}
placeholder="Enter duration"
addonAfter={
<Select
value={durationUnit}
onChange={(v): void => setDurationUnit(v)}
options={DURATION_UNIT_OPTIONS}
/>
}
onWheel={(e): void => e.currentTarget.blur()}
/>
</Form.Item>
)}
<div className="mute-scheduler-drawer__callout">
<Info size={14} color="var(--bg-aqua-500)" />
<p>
The rule will <strong>keep evaluating</strong> and firing alerts to the
History tab. Only notifications (Slack, PagerDuty, email) are silenced.
</p>
</div>
<div className="mute-scheduler-drawer__footer">
<Button type="text" onClick={onClose} disabled={isLoading}>
Cancel
</Button>
<Button
type="primary"
htmlType="submit"
loading={isLoading}
icon={<Check size={14} />}
>
Mute alert
</Button>
</div>
</Form>
</Drawer>
);
}
export default MuteSchedulerDrawer;

View File

@@ -1,104 +0,0 @@
import { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { BellOff } from '@signozhq/icons';
import ROUTES from 'constants/routes';
import dayjs from 'dayjs';
import type { ActiveMute } from './useActiveMutes';
import './StateBanners.styles.scss';
const PLANNED_DOWNTIMES_URL = `${ROUTES.LIST_ALL_ALERT}?tab=PlannedDowntime`;
const formatRemaining = (endTime: string): string | null => {
const end = dayjs(endTime);
const now = dayjs();
const diffMs = end.diff(now);
if (diffMs <= 0) {
return null;
}
const totalMinutes = Math.floor(diffMs / 60000);
const days = Math.floor(totalMinutes / (60 * 24));
const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
const minutes = totalMinutes % 60;
if (days > 0) {
return `${days}d ${hours}h LEFT`;
}
if (hours > 0) {
return `${hours}h ${minutes}m LEFT`;
}
return `${minutes}m LEFT`;
};
interface MutedBannerProps {
activeMute: ActiveMute;
onExpire?: () => void;
}
function MutedBanner({ activeMute, onExpire }: MutedBannerProps): JSX.Element {
const endTime = activeMute.end;
const [remaining, setRemaining] = useState(
endTime ? formatRemaining(endTime) : null,
);
useEffect(() => {
if (!endTime) {
return;
}
const interval = setInterval(() => {
const next = formatRemaining(endTime);
setRemaining(next);
if (next === null) {
clearInterval(interval);
onExpire?.();
}
}, 60_000);
return (): void => clearInterval(interval);
}, [endTime, onExpire]);
const titleText = useMemo(() => {
if (!endTime) {
return 'Notifications muted';
}
return `Notifications muted until ${dayjs(endTime).format('MMM D, h:mm A')}`;
}, [endTime]);
const reason = activeMute.description || activeMute.name;
return (
<div className="state-banner state-banner--muted" role="status">
<div className="state-banner__icon-disc state-banner__icon-disc--muted">
<BellOff size={18} color="var(--bg-amber-500)" />
</div>
<div className="state-banner__body">
<div className="state-banner__title">
<span>{titleText}</span>
{remaining && (
<span className="state-banner__pill state-banner__pill--muted">
{remaining}
</span>
)}
</div>
<div className="state-banner__meta">
<span>Rule is still evaluating alerts will appear in History</span>
{reason && (
<>
{' · '}
<span>
Name: <strong>{reason}</strong>
</span>
</>
)}
{' · '}
<Link to={PLANNED_DOWNTIMES_URL} className="state-banner__link">
Manage in Planned Downtimes
</Link>
</div>
</div>
</div>
);
}
export default MutedBanner;

View File

@@ -1,98 +0,0 @@
.state-banner {
display: flex;
gap: 14px;
align-items: center;
margin-top: 16px;
padding: 12px 16px;
border-radius: 8px;
font-family: 'Inter', sans-serif;
&--muted {
background: linear-gradient(
90deg,
rgba(255, 205, 86, 0.1),
rgba(255, 205, 86, 0.04)
);
border: 1px solid rgba(255, 205, 86, 0.25);
}
&--disabled {
background: rgba(98, 104, 124, 0.06);
border: 1px solid var(--bg-slate-200);
}
&__icon-disc {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 999px;
flex-shrink: 0;
&--muted {
background: rgba(255, 205, 86, 0.15);
}
&--disabled {
background: rgba(98, 104, 124, 0.15);
}
}
&__body {
flex: 1;
min-width: 0;
}
&__title {
display: flex;
align-items: center;
gap: 8px;
font-size: 13.5px;
font-weight: 600;
color: var(--bg-vanilla-100);
font-variant-numeric: tabular-nums;
}
&__pill {
display: inline-flex;
align-items: center;
padding: 2px 7px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
border-radius: 4px;
&--muted {
color: var(--bg-amber-500);
background: rgba(255, 205, 86, 0.12);
}
&--disabled {
color: var(--bg-slate-50);
background: rgba(98, 104, 124, 0.18);
}
}
&__meta {
margin-top: 4px;
font-size: 12px;
line-height: 1.5;
color: var(--bg-vanilla-400);
strong {
color: var(--bg-vanilla-100);
font-weight: 600;
}
}
&__link {
color: var(--bg-robin-500);
&:hover {
color: var(--bg-robin-400);
text-decoration: underline;
}
}
}

View File

@@ -1,30 +0,0 @@
import { useMemo } from 'react';
import { useGetRuleByID } from 'api/generated/services/rules';
import type { RuletypesActiveMuteDTO } from 'api/generated/services/sigNoz.schemas';
export type ActiveMute = RuletypesActiveMuteDTO;
type UseActiveMuteResult = {
activeMutes: ActiveMute[];
isLoading: boolean;
isFetching: boolean;
refetch: () => void;
};
export const useActiveMutes = (
ruleId: string | undefined,
): UseActiveMuteResult => {
const { data, isLoading, isFetching, refetch } = useGetRuleByID(
{ id: ruleId || '' },
{
query: {
enabled: Boolean(ruleId),
refetchOnWindowFocus: false,
},
},
);
const activeMutes = useMemo(() => data?.data?.mutes ?? [], [data]);
return { activeMutes, isLoading, isFetching, refetch };
};

View File

@@ -1,92 +0,0 @@
import { useCallback } from 'react';
import { useMutation, useQueryClient } from 'react-query';
import {
createDowntimeSchedule,
getListDowntimeSchedulesQueryKey,
} from 'api/generated/services/downtimeschedules';
import {
getGetRuleByIDQueryKey,
getListRulesQueryKey,
} from 'api/generated/services/rules';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import type {
AlertmanagertypesPostablePlannedMaintenanceDTO,
AlertmanagertypesRecurrenceDTO,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { useNotifications } from 'hooks/useNotifications';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
export type MutePayload = {
name: string;
startTime: string;
endTime?: string | null;
timezone: string;
recurrence?: AlertmanagertypesRecurrenceDTO;
};
type UseMuteAlertRuleArgs = {
ruleId: string;
onSuccess?: () => void;
};
type UseMuteAlertRuleResult = {
mute: (payload: MutePayload) => Promise<void>;
isLoading: boolean;
};
export const useMuteAlertRule = ({
ruleId,
onSuccess,
}: UseMuteAlertRuleArgs): UseMuteAlertRuleResult => {
const { notifications } = useNotifications();
const { showErrorModal } = useErrorModal();
const queryClient = useQueryClient();
const { mutateAsync, isLoading } = useMutation(
['createMuteDowntime', ruleId],
(payload: AlertmanagertypesPostablePlannedMaintenanceDTO) =>
createDowntimeSchedule(payload),
{
onSuccess: () => {
void queryClient.invalidateQueries(getListDowntimeSchedulesQueryKey());
void queryClient.invalidateQueries(getGetRuleByIDQueryKey({ id: ruleId }));
void queryClient.invalidateQueries(getListRulesQueryKey());
notifications.success({ message: 'Alert muted' });
onSuccess?.();
},
onError: (error) => {
showErrorModal(
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
);
},
},
);
const mute = useCallback(
async (payload: MutePayload): Promise<void> => {
if (!ruleId) {
return;
}
const body: AlertmanagertypesPostablePlannedMaintenanceDTO = {
name: payload.name,
alertIds: [ruleId],
schedule: {
startTime: payload.startTime,
// null = no end ("Forever"). The generated type narrows endTime to
// string, but the API accepts null to mean indefinite.
endTime:
payload.endTime === null ? (null as unknown as string) : payload.endTime,
timezone: payload.timezone,
recurrence: payload.recurrence,
},
};
await mutateAsync(body);
},
[mutateAsync, ruleId],
);
return { mute, isLoading };
};

View File

@@ -7,10 +7,11 @@ 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_TAB = 'PlannedDowntime';
const ROUTING_POLICIES_TAB = 'RoutingPolicies';
const PLANNED_DOWNTIME_SUB_TAB = 'planned-downtime';
const ROUTING_POLICIES_SUB_TAB = 'routing-policies';
const mockUseLocation = jest.fn();
jest.mock('react-router-dom', () => ({
@@ -121,7 +122,7 @@ describe('AlertList', () => {
expect(screen.getByText(LIST_ALERT_RULES_TEXT)).toBeInTheDocument();
});
it('should render all four top-level tabs', () => {
it('should render all three main tabs', () => {
mockQueryParams({});
mockLocation(ALERTS_PATH);
@@ -129,8 +130,7 @@ describe('AlertList', () => {
expect(screen.getByText(TRIGGERED_ALERTS_TEXT)).toBeInTheDocument();
expect(screen.getByText(ALERT_RULES_TEXT)).toBeInTheDocument();
expect(screen.getByText(PLANNED_DOWNTIME_TEXT)).toBeInTheDocument();
expect(screen.getByText(ROUTING_POLICIES_TEXT)).toBeInTheDocument();
expect(screen.getByText(CONFIGURATION_TEXT)).toBeInTheDocument();
});
});
@@ -153,22 +153,13 @@ describe('AlertList', () => {
expect(screen.getByText(LIST_ALERT_RULES_TEXT)).toBeInTheDocument();
});
it('should render PlannedDowntime tab when tab query param is PlannedDowntime', () => {
mockQueryParams({ tab: PLANNED_DOWNTIME_TAB });
it('should render Configuration tab with default Planned Downtime sub-tab when tab query param is Configuration', () => {
mockQueryParams({ tab: 'Configuration' });
mockLocation(ALERTS_PATH);
render(<AlertList />);
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();
expect(screen.getByText(PLANNED_DOWNTIME_TEXT)).toBeInTheDocument();
});
it('should navigate to TriggeredAlerts tab when clicked', () => {
@@ -184,32 +175,89 @@ describe('AlertList', () => {
expect(mockSafeNavigate).toHaveBeenCalledWith('/alerts?tab=TriggeredAlerts');
});
it('should navigate to PlannedDowntime tab when clicked', () => {
mockQueryParams({ tab: 'AlertRules' });
it('should navigate to AlertRules tab when clicked', () => {
mockQueryParams({ tab: 'TriggeredAlerts' });
mockLocation(ALERTS_PATH);
render(<AlertList />);
clickTab(PLANNED_DOWNTIME_TEXT);
clickTab(ALERT_RULES_TEXT);
expect(mockSet).toHaveBeenCalledWith('tab', PLANNED_DOWNTIME_TAB);
expect(mockSafeNavigate).toHaveBeenCalledWith(
`/alerts?tab=${PLANNED_DOWNTIME_TAB}`,
);
expect(mockSet).toHaveBeenCalledWith('tab', 'AlertRules');
expect(mockDelete).toHaveBeenCalledWith('subTab');
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();
});
});
it('should navigate to RoutingPolicies tab when clicked', () => {
mockQueryParams({ tab: 'AlertRules' });
mockLocation(ALERTS_PATH);
describe('Navigation', () => {
it('should navigate to Configuration tab with default subTab when clicked', () => {
mockQueryParams({ tab: 'AlertRules' });
mockLocation(ALERTS_PATH);
render(<AlertList />);
render(<AlertList />);
clickTab(ROUTING_POLICIES_TEXT);
clickTab(CONFIGURATION_TEXT);
expect(mockSet).toHaveBeenCalledWith('tab', ROUTING_POLICIES_TAB);
expect(mockSafeNavigate).toHaveBeenCalledWith(
`/alerts?tab=${ROUTING_POLICIES_TAB}`,
);
expect(mockSet).toHaveBeenCalledWith('tab', CONFIGURATION_TEXT);
expect(mockSet).toHaveBeenCalledWith('subTab', PLANNED_DOWNTIME_SUB_TAB);
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(mockDelete).toHaveBeenCalledWith('subTab');
expect(mockSafeNavigate).toHaveBeenCalledWith('/alerts?tab=AlertRules');
});
});
});
});

View File

@@ -1,3 +1,4 @@
import { useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { Tabs, TabsProps } from 'antd';
import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon';
@@ -9,10 +10,10 @@ import RoutingPolicies from 'container/RoutingPolicies';
import TriggeredAlerts from 'container/TriggeredAlerts';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { CalendarClock, GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
import { GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
import AlertDetails from 'pages/AlertDetails';
import { AlertListTabs } from './types';
import { AlertListSubTabs, AlertListTabs } from './types';
import './AlertList.styles.scss';
@@ -22,9 +23,43 @@ 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 => {
urlQuery.set('tab', AlertListTabs.CONFIGURATION);
urlQuery.set('subTab', subTab);
urlQuery.delete('search');
safeNavigate(`/alerts?${urlQuery.toString()}`);
},
[safeNavigate, urlQuery],
);
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: (
@@ -50,25 +85,15 @@ function AllAlertList(): JSX.Element {
</div>
),
},
{
label: (
<div className="periscope-tab top-level-tab">
<CalendarClock size={14} />
Planned Downtime
</div>
),
key: AlertListTabs.PLANNED_DOWNTIME,
children: <PlannedDowntime />,
},
{
label: (
<div className="periscope-tab top-level-tab">
<ConfigureIcon width={14} height={14} />
Routing Policies
Configuration
</div>
),
key: AlertListTabs.ROUTING_POLICIES,
children: <RoutingPolicies />,
key: AlertListTabs.CONFIGURATION,
children: configurationTab,
},
];
@@ -77,10 +102,21 @@ function AllAlertList(): JSX.Element {
destroyInactiveTabPane
items={items}
activeKey={tab || AlertListTabs.ALERT_RULES}
onChange={(nextTab): void => {
urlQuery.set('tab', nextTab);
urlQuery.delete('subTab');
onChange={(tab): void => {
urlQuery.set('tab', tab);
// If navigating to Configuration tab, set default subTab
if (tab === AlertListTabs.CONFIGURATION) {
const currentSubTab = subTab || AlertListSubTabs.PLANNED_DOWNTIME;
urlQuery.set('subTab', currentSubTab);
} else {
// Clear subTab when navigating out of Configuration tab
urlQuery.delete('subTab');
}
// Clear search when navigating to any tab
urlQuery.delete('search');
safeNavigate(`/alerts?${urlQuery.toString()}`);
}}
className={`alerts-container ${

View File

@@ -1,6 +1,10 @@
export enum AlertListSubTabs {
PLANNED_DOWNTIME = 'planned-downtime',
ROUTING_POLICIES = 'routing-policies',
}
export enum AlertListTabs {
TRIGGERED_ALERTS = 'TriggeredAlerts',
ALERT_RULES = 'AlertRules',
PLANNED_DOWNTIME = 'PlannedDowntime',
ROUTING_POLICIES = 'RoutingPolicies',
CONFIGURATION = 'Configuration',
}

View File

@@ -1,5 +1,6 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { Input, Select } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Select } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import './DropRateView.styles.scss';

View File

@@ -1,26 +1,3 @@
.service-route-tab {
margin-bottom: 64px;
.ant-tabs-nav {
&::before {
border-bottom: 1px solid var(--l1-border);
}
.ant-tabs-nav-wrap {
.ant-tabs-nav-list {
.ant-tabs-ink-bar {
background-color: var(--primary-background) !important;
}
.ant-tabs-tab {
font-size: 13px;
font-family: 'Inter';
color: var(--l1-foreground);
line-height: 20px;
letter-spacing: -0.07px;
gap: 10px;
}
.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
color: var(--accent-primary);
}
}
}
}
}

View File

@@ -1,5 +1,5 @@
import { useParams } from 'react-router-dom';
import { Tabs, TabsProps } from 'antd';
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
import { QueryParams } from 'constants/query';
import DBCall from 'container/MetricsApplication/Tabs/DBCall';
import External from 'container/MetricsApplication/Tabs/External';
@@ -24,7 +24,7 @@ function MetricsApplication(): JSX.Element {
const urlQuery = useUrlQuery();
const { safeNavigate } = useSafeNavigate();
const items: TabsProps['items'] = [
const items: TabItemProps[] = [
{
label: TAB_KEY_VS_LABEL[MetricsApplicationTab.OVER_METRICS],
key: MetricsApplicationTab.OVER_METRICS,
@@ -53,9 +53,8 @@ function MetricsApplication(): JSX.Element {
<ApDexApplication />
<Tabs
items={items}
activeKey={activeKey}
value={activeKey}
className="service-route-tab"
destroyInactiveTabPane
onChange={onTabChange}
/>
</div>

View File

@@ -1,9 +0,0 @@
.pipeline-tabs {
.ant-tabs-content {
padding: 0 16px;
}
.ant-tabs-tabpane-hidden {
display: none !important;
}
}

View File

@@ -2,8 +2,7 @@ import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import * as Sentry from '@sentry/react';
import type { TabsProps } from 'antd';
import { Tabs } from 'antd';
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
import getPipeline from 'api/pipeline/get';
import Spinner from 'components/Spinner';
import ChangeHistory from 'container/PipelinePage/Layouts/ChangeHistory';
@@ -13,8 +12,6 @@ import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFall
import { SuccessResponse } from 'types/api';
import { Pipeline } from 'types/api/pipeline/def';
import './Pipelines.styles.scss';
const pipelineRefetchInterval = (
pipelineResponse: SuccessResponse<Pipeline> | undefined,
): number | false => {
@@ -46,7 +43,7 @@ function Pipelines(): JSX.Element {
refetchInterval: pipelineRefetchInterval,
});
const tabItems: TabsProps['items'] = useMemo(
const tabItems: TabItemProps[] = useMemo(
() => [
{
key: 'pipelines',
@@ -83,11 +80,7 @@ function Pipelines(): JSX.Element {
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<Tabs
className="pipeline-tabs"
defaultActiveKey="pipelines"
items={tabItems}
/>
<Tabs defaultValue="pipelines" items={tabItems} />
</Sentry.ErrorBoundary>
);
}

View File

@@ -28,6 +28,10 @@
.learn-more {
font-size: 14px;
}
.search-input-container {
margin-top: 16px;
margin-bottom: 8px;
}
.ant-input-affix-wrapper {
margin-top: 16px;

View File

@@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { Color } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { ColorPicker, Input, Modal, Table, TableProps } from 'antd';
import { Input } from '@signozhq/ui/input';
import { ColorPicker, Modal, Table, TableProps } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import {
@@ -311,12 +312,15 @@ function SaveView(): JSX.Element {
Learn more
</Typography.Link>
</Typography.Text>
<Input
placeholder="Search for views..."
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
value={searchValue}
onChange={handleSearch}
/>
<div className="search-input-container">
<Input
placeholder="Search for views..."
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
value={searchValue}
onChange={handleSearch}
className="search-input"
/>
</div>
<Table
columns={columns}

View File

@@ -340,7 +340,7 @@ function SettingsPage(): JSX.Element {
routes={routes}
activeKey={pathname}
history={history}
tabBarStyle={{ display: 'none' }}
hideTabBar
/>
</div>
</div>

View File

@@ -71,88 +71,6 @@
flex-direction: column;
gap: 25px;
padding-top: 16px;
.flamegraph-waterfall-toggle {
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
height: 31px;
color: var(--l2-foreground);
padding: 5px 20px;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.ant-btn-icon {
margin-inline-end: 0px !important;
}
}
.span-list-toggle {
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
height: 31px;
padding: 5px 20px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.ant-btn-icon {
margin-inline-end: 0px !important;
}
}
.trace-visualisation-tabs {
.ant-tabs-tab {
border-radius: 2px 0px 0px 0px;
background: var(--l2-background);
border-radius: 2px 2px 0px 0px;
border: 1px solid var(--l1-border);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
height: 31px;
}
.ant-tabs-tab-active {
background-color: var(--l1-background);
.ant-btn {
color: var(--l1-foreground) !important;
}
}
.ant-tabs-tab + .ant-tabs-tab {
margin: 0px;
border-left: 0px;
}
.ant-tabs-ink-bar {
height: 1px !important;
background: var(--l1-background) !important;
}
.ant-tabs-nav-list {
transform: translate(15px, 0px) !important;
}
.ant-tabs-nav::before {
border-bottom: 1px solid var(--l1-border);
}
.ant-tabs-nav {
margin: 0px;
padding: 0px !important;
}
}
}
}
}

View File

@@ -5,7 +5,7 @@ import {
ResizablePanel,
ResizablePanelGroup,
} from '@signozhq/resizable';
import { Button, Tabs } from 'antd';
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
import FlamegraphImg from 'assets/TraceDetail/Flamegraph';
import cx from 'classnames';
import TraceFlamegraph from 'container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph';
@@ -86,18 +86,11 @@ function TraceDetailsV2(): JSX.Element {
}
}, [noData]);
const items = [
const items: TabItemProps[] = [
{
label: (
<Button
type="text"
icon={<FlamegraphImg />}
className="flamegraph-waterfall-toggle"
>
Flamegraph
</Button>
),
key: 'flamegraph',
label: 'Flamegraph',
prefixIcon: <FlamegraphImg />,
children: (
<>
<TraceFlamegraph
@@ -145,11 +138,7 @@ function TraceDetailsV2(): JSX.Element {
totalSpans={traceData?.payload?.totalSpansCount || 0}
notFound={noData}
/>
{!noData ? (
<Tabs items={items} animated className="trace-visualisation-tabs" />
) : (
<NoData />
)}
{!noData ? <Tabs items={items} /> : <NoData />}
</ResizablePanel>
<ResizableHandle withHandle className="resizable-handle" />

Some files were not shown because too many files have changed in this diff Show More