mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-30 05:40:33 +01:00
Compare commits
15 Commits
mute-rules
...
refactor/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36417a5f9e | ||
|
|
989b1252df | ||
|
|
51cb119f79 | ||
|
|
180a2c067f | ||
|
|
83351ca01d | ||
|
|
b11e2af392 | ||
|
|
7f6e89ea22 | ||
|
|
8aeb9b5a77 | ||
|
|
46c8f3579e | ||
|
|
9ff045482f | ||
|
|
910645516d | ||
|
|
edc1278769 | ||
|
|
da1b09c479 | ||
|
|
1f406823d8 | ||
|
|
f626380b1a |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
12
frontend/src/components/RouteTab/RouteTab.styles.scss
Normal file
12
frontend/src/components/RouteTab/RouteTab.styles.scss
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const InputComponent = styled(Input)`
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 ${
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.pipeline-tabs {
|
||||
.ant-tabs-content {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.ant-tabs-tabpane-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -340,7 +340,7 @@ function SettingsPage(): JSX.Element {
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
tabBarStyle={{ display: 'none' }}
|
||||
hideTabBar
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user