Compare commits

..

33 Commits

Author SHA1 Message Date
Jatinderjit Singh
4da4569953 Merge branch 'main' into mute-rules 2026-05-29 06:53:51 +05:30
Jatinderjit Singh
9458928efc fix(alerts): give mute drawer date picker a solid popup background
The DatePicker popup is portaled to <body>, so without a panel-container
background the time-column cells were transparent and the drawer content
bled through.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 06:49:35 +05:30
Jatinderjit Singh
f713254f59 fix: link to planned downtime page 2026-05-29 06:40:06 +05:30
Jatinderjit Singh
eb5f0e946a fix(alerts): format mute times in selected timezone
The backend ignores the offset in the formatted startTime/endTime and
re-attaches the input timezone to the raw wall-clock time. So the
formatted offset must match the timezone field, otherwise the muted
window shifts. Mirror PlannedDowntimeForm by formatting in the selected
timezone: the quick-duration popover formats in the guessed tz, and the
custom-window drawer reinterprets the picked time via .tz(tz, true).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 06:29:05 +05:30
Jatinderjit Singh
fdb48834da fix: return correct upcoming status for recurring maintenances 2026-05-29 06:16:23 +05:30
Jatinderjit Singh
e46d26ff78 refactor: drop status filter, keep status badges 2026-05-29 05:56:41 +05:30
Jatinderjit Singh
503f5c3e7b feat(planned-downtime): add status filter and badge
Planned downtimes were rendered as a flat list with no visual cue for
which were active, upcoming, or expired. Add an "Active & Upcoming /
Expired / All" filter (defaulting to Active & Upcoming so expired noise
is hidden) and a status badge on each row. Sort by status (active →
upcoming → expired) then by most recently updated within each group.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 05:42:07 +05:30
Jatinderjit Singh
de2a730557 refactor(alerts): promote Planned Downtime and Routing Policies to top-level tabs
Replace the nested Configuration > {Planned Downtime, Routing Policies}
sub-tab structure with four flat top-level tabs on /alerts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 05:23:48 +05:30
Jatinderjit Singh
79deebe976 fix(planned-downtime): cascade delete associated rules
Deleting a planned maintenance previously failed with a foreign key
error when alert rules were associated with it, forcing users to first
detach every rule. Wrap the delete in a transaction that first removes
rows from planned_maintenance_rule before deleting the maintenance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 05:11:21 +05:30
Jatinderjit Singh
372cdc89e1 refactor(planned-maintenance): simplify IsActive and IsUpcoming 2026-05-29 04:59:59 +05:30
Jatinderjit Singh
bb5e68eef6 feat(planned-maintenance): support indefinite ("forever") fixed downtimes
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 04:59:07 +05:30
Jatinderjit Singh
a2f4ce934d Merge branch 'main' into mute-rules 2026-05-27 17:39:00 +05:30
Jatinderjit Singh
c2419a8687 fix: refresh disabled-since time in DisabledBanner every 60s 2026-05-27 17:29:11 +05:30
Jatinderjit Singh
7c214be7ad fix: hide muted banner when mute expires 2026-05-27 17:15:35 +05:30
Jatinderjit Singh
27a2a0cf21 refactor: return all active mutes 2026-05-27 16:33:16 +05:30
Jatinderjit Singh
6ec3c0b8e0 fix: handle badge and banner for indefinite mutes 2026-05-27 11:29:34 +05:30
Jatinderjit Singh
fef84df331 feat(alerts): return activeMute in rule API responses
Backend computes the active mute window per rule (joining with planned
maintenance schedules) in ListRules and GetRuleByID, so the frontend no
longer needs a separate downtime-schedules fetch to determine mute state.

- Add ActiveMuteInfo struct to Rule (id, name, description,
  effectiveStartTime, effectiveEndTime); computed by findActiveMuteForRule
- Handlers for ListRules/GetRuleByID now fetch MaintenanceStore schedules
  and pass them to NewRule
- Regenerate OpenAPI spec and frontend types (RuletypesActiveMuteInfoDTO)
- useActiveMute: drop useListDowntimeSchedules, read activeMute from
  useGetRuleByID (cache-hit on detail page; one request on list page)
- useMuteAlertRule: also invalidate rule queries after muting so activeMute
  refreshes without a separate schedules refetch
- ListAlert: remove useListDowntimeSchedules + findActiveMuteForRule,
  read record.activeMute.effectiveEndTime directly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 20:56:47 +05:30
Jatinderjit Singh
98813660ed fix(alerts): send endTime as null for "Forever" mute
Previously, picking Forever set endTime to now + 10 years so the
schedule had a real (very distant) end. Send null instead so the
backend treats the mute as truly indefinite. The generated schema
narrows endTime to string, but the API accepts null — cast at the
call site with a comment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:51:03 +05:30
Jatinderjit Singh
cd37fbfa71 fix(alerts): drop focus after closing mute popover via Esc / outside click
Pressing Escape promotes the most recent input to 'keyboard', so the
trigger button (Mute) showed a :focus-visible outline once the popover
closed. Blur the active element when closing via keyboard or outside
click so no leftover focus ring lingers. Tab-driven focus still shows
the indicator as expected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:51:03 +05:30
Jatinderjit Singh
fca2da6b15 fix(alerts): close mute popover on outside click and Escape
Since the Popover uses trigger={[]} (controlled-only), antd no longer
attaches its own outside-click / Escape handlers. Add document-level
mousedown and keydown listeners while the popover is open, deferring
attachment by one tick so the click that opened it isn't counted as
an outside click.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:51:03 +05:30
Jatinderjit Singh
4a2907ad35 fix(alerts): show firing state + muted badge separately in rule list
Match the original handoff: Status column keeps showing the actual
rule state (Firing/OK/Pending/Disabled), and a separate inline
'MUTED · <countdown>' badge renders next to the rule name when an
active mute exists. Both signals stay visible.

- Revert Status.tsx to its original simple form (no muteEndTime).
- Extract the muted badge into MutedBadge.tsx and render it inline
  in the Alert Name column.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:51:03 +05:30
Jatinderjit Singh
6b04e17f64 fix(alerts): remove 'Muted/Disabled by <user>' from banners
Drop the createdBy/updatedBy attribution from both the muted and
disabled banners. MutedBanner keeps name + manage link;
DisabledBanner keeps the relative time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:51:03 +05:30
Jatinderjit Singh
95864ebe58 fix(alerts): disable Mute pill when alert rule is disabled
Muting a disabled rule wouldn't change observable behavior — fires
aren't recorded, so there's nothing to suppress. Disabling the Mute
pill also brings cursor + hover affordances in line with the
non-interactive Active pill while muted: cursor: not-allowed and no
hover background (the SCSS already guards hover with :not(:disabled)).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:51:03 +05:30
Jatinderjit Singh
a0b9713a4f feat(alerts): add hover background to segmented pills
Surface clickable regions more clearly:
- Inactive pill hover: --bg-ink-200 background (dark) / --bg-vanilla-300
  (light mode).
- Active pill hovers darken slightly to their next shade — robin-600,
  amber-600, slate-200 — for tactile feedback without losing the
  state color.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:51:03 +05:30
Jatinderjit Singh
bded46ce95 fix(alerts): wrap segmented pill text in span for reliable color
The previous attempt set color on the .pill--active-muted block but
something in the cascade was leaving the bare text node light while
the icon (which had its own explicit rule) went dark. Wrap each pill's
label in <span class="alert-state-segmented__label"> and apply the
muted color to both the icon and label classes — both children now
have explicit color rules that don't depend on inheritance from the
button.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:51:03 +05:30
Jatinderjit Singh
b38d2963b5 fix(alerts): make muted segmented pill text legible
The base .alert-state-segmented__pill rule was winning over
.alert-state-segmented__pill--active-muted in the cascade for some
build configurations, leaving the "Mute" label rendered in
var(--bg-vanilla-400) (light gray) on amber. Bump specificity by
self-chaining the class (&.alert-state-segmented__pill--active-muted →
.pill.pill--active-muted) so the dark text color always wins, and
apply the color to the icon explicitly so it tracks the label.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:51:03 +05:30
Jatinderjit Singh
406800a2e5 Revert "fix(alerts): move toggle toast to per-call mutate callbacks"
This reverts commit 7dacb99536ca386a9b74d101f2f84fdd39ce6864.
2026-05-25 19:51:03 +05:30
Jatinderjit Singh
b10f45a59a Revert "fix(alerts): update rule cache directly instead of refetching"
This reverts commit adc9e0ff1162d7dcc1f82ccfd54ae3b1c428cc46.
2026-05-25 19:51:03 +05:30
Jatinderjit Singh
f75d166eb6 fix(alerts): update rule cache directly instead of refetching
invalidateGetRuleByID + refetchQueries caused EditRules to render its
<Spinner /> (isRefetching=true), which unmounted the whole form
subtree. The unmount cascade plus follow-up query resolutions (channels,
event, fields/keys) caused the success toast to briefly disappear and
re-enter from its animation again, visible as a flicker.

Patching the rule already returns the updated rule, so write it into
the query cache via setQueryData. The form sees the new state without
remounting, and the toast stays put.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:51:03 +05:30
Jatinderjit Singh
81f7694991 fix(alerts): move toggle toast to per-call mutate callbacks
Hook-level onSuccess on useMutation can re-fire when the
MutationObserver re-subscribes during context-driven re-renders (each
context update from setAlertRuleState re-renders all consumers of
useAlertRule, including ActionButtons, which re-instantiates the
mutation observer). Per-call callbacks passed to mutate() in
react-query v3 are guaranteed to run exactly once per mutate() call.

This was visible as the success toast appearing once per follow-up API
response after enable/disable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:51:03 +05:30
Jatinderjit Singh
3858e70a4c fix(alerts): detach mute popover from segmented control anchor
Popover's Trigger wrapper could intercept clicks on the inner buttons,
causing the disable/enable toggle to fire twice (visible as a flickering
success toast). Render the segmented control standalone and anchor the
Popover to a separate invisible span positioned at the bottom-right of
the wrapper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:51:03 +05:30
Jatinderjit Singh
9b87348839 fix(alerts): mute popover only opens via Mute pill, not Active/Disable
The Popover wrapper used trigger="click", which fired on any click in
the anchor (including Active and Disable pills). Switch to trigger={[]}
so the popover is controlled exclusively by the Mute handler.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:51:02 +05:30
Jatinderjit Singh
fda747f81e feat(alerts): add mute action wrapping planned downtime
Adds a Mute action on the alert rule page that creates a Planned
Downtime entry scoped to the rule, with a quick-duration popover and
full scheduler drawer.

- AlertStateSegmented control replaces the Active/Disabled switch with a
  three-state pill (Active / Mute / Disable). Active is disabled while
  muted; un-mute happens via the Planned Downtimes page.
- MutePopover offers quick durations (15m, 1h, 4h, 1d, 1w, Forever) plus
  a Name field.
- MuteSchedulerDrawer exposes the full Planned Downtime form for custom
  windows and recurrence.
- MutedBanner / DisabledBanner render an informative banner under the
  header for the corresponding states.
- Alert rules list shows a muted badge with countdown for each rule that
  has an active downtime.
- Lookup is frontend-only via listDowntimeSchedules + ruleId filter,
  with no backend changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:51:02 +05:30
148 changed files with 3023 additions and 1486 deletions

View File

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

View File

@@ -162,11 +162,21 @@ 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 {
@@ -180,7 +190,7 @@ export interface AlertmanagertypesScheduleDTO {
* @type string
* @format date-time
*/
startTime: string;
startTime?: string;
/**
* @type string
*/
@@ -7041,6 +7051,31 @@ 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',
@@ -7409,6 +7444,10 @@ export interface RuletypesRuleDTO {
* @type object
*/
labels?: RuletypesRuleDTOLabels;
/**
* @type array,null
*/
mutes?: RuletypesActiveMuteDTO[] | null;
notificationSettings?: RuletypesNotificationSettingsDTO;
/**
* @type array

View File

@@ -41,22 +41,14 @@ $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;
&:focus,
&:focus-visible,
&:hover {
border: none;
&.ant-input:focus {
box-shadow: none;
outline: none;
}
&::placeholder {

View File

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

View File

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

View File

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

View File

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

View File

@@ -266,14 +266,6 @@
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;
}
}
}
}
@@ -299,21 +291,5 @@
.cm-placeholder {
font-size: 12px !important;
}
$add-on-row-height: 38px;
.periscope-input-with-label {
.input {
.ant-select {
height: $add-on-row-height;
}
}
}
.input-with-label {
.input {
height: $add-on-row-height;
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQue
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { AlertListTabs } from 'pages/AlertList/types';
import { GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
import { CalendarClock, GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef } from 'types/api/alerts/def';
@@ -175,11 +175,21 @@ function CreateRules(): JSX.Element {
{
label: (
<div className="periscope-tab top-level-tab">
<ConfigureIcon width={14} height={14} />
Configuration
<CalendarClock size={14} />
Planned Downtime
</div>
),
key: AlertListTabs.CONFIGURATION,
key: AlertListTabs.PLANNED_DOWNTIME,
children: null,
},
{
label: (
<div className="periscope-tab top-level-tab">
<ConfigureIcon width={14} height={14} />
Routing Policies
</div>
),
key: AlertListTabs.ROUTING_POLICIES,
children: null,
},
];

View File

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

View File

@@ -4,5 +4,4 @@ export const THRESHOLD_TAB_TOOLTIP =
export const ANOMALY_TAB_TOOLTIP =
'An alert is triggered whenever the metric deviates from an expected pattern.';
export const ROUTING_POLICIES_ROUTE =
'/alerts?tab=Configuration&subTab=routing-policies';
export const ROUTING_POLICIES_ROUTE = '/alerts?tab=RoutingPolicies';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
import { Button, Tabs, Tooltip } from 'antd';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { Braces, Globe, Table } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
@@ -9,6 +9,8 @@ import DashboardVariableSettings from './DashboardVariableSettings';
import GeneralDashboardSettings from './General';
import PublicDashboardSetting from './PublicDashboard';
import './DashboardSettingsContent.styles.scss';
function DashboardSettings({
variablesSettingsTabHandle,
}: {
@@ -19,26 +21,49 @@ function DashboardSettings({
const enablePublicDashboard = isCloudUser || isEnterpriseSelfHostedUser;
const publicDashboardItem: TabItemProps = {
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>
),
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: TabItemProps[] = [
const items = [
{
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}
@@ -48,7 +73,7 @@ function DashboardSettings({
...(enablePublicDashboard ? [publicDashboardItem] : []),
];
return <Tabs items={items} />;
return <Tabs items={items} animated className="settings-tabs" />;
}
export default DashboardSettings;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -43,6 +43,7 @@ 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';
@@ -276,7 +277,14 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
onEditHandler(record, { newTab: isModifierKeyPressed(e) });
};
return <Typography.Link onClick={onClickHandler}>{value}</Typography.Link>;
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>
);
},
sortOrder: sortedInfo.columnKey === 'name' ? sortedInfo.order : null,
},

View File

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

View File

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

View File

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

View File

@@ -12,9 +12,17 @@ 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, Modal, Popover, Skeleton, Table, Tooltip } from 'antd';
import {
Button,
Flex,
Input,
Modal,
Popover,
Skeleton,
Table,
Tooltip,
} from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';

View File

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

View File

@@ -2,9 +2,8 @@ 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 } from 'antd';
import { Collapse, Input } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { Badge } from '@signozhq/ui/badge';
import { Typography } from '@signozhq/ui/typography';

View File

@@ -2,8 +2,7 @@ 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 { Input } from '@signozhq/ui/input';
import { Col } from 'antd';
import { Col, Input } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import { AppState } from 'store/reducers';

View File

@@ -2,8 +2,7 @@ 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 { Input } from '@signozhq/ui/input';
import { Button, Select } from 'antd';
import { Button, Input, Select } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
import {
ConditionalOperators,

View File

@@ -1,7 +1,6 @@
import { Input } from 'antd';
import { Typography } from '@signozhq/ui/typography';
// 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 { Select } from 'antd';
import classNames from 'classnames';
import { TIME_AGGREGATION_OPTIONS } from './constants';

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
// 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 { Button, Col, Form, Input as AntInput, Input, Row } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { CONTEXT_LINK_FIELDS } from 'container/NewWidget/RightContainer/ContextLinks/constants';
import {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
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, Space, Tooltip } from 'antd';
import { Flex, Form, Input, Space, Tooltip } from 'antd';
import { ProcessorData } from 'types/api/pipeline/def';
import { PREDEFINED_MAPPING } from '../config';

View File

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

View File

@@ -2,8 +2,7 @@ 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 { Input } from '@signozhq/ui/input';
import { Button, Flex, Form, Tooltip } from 'antd';
import { Button, Flex, Form, Input, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import {
useDeleteDowntimeScheduleByID,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
// 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 { Typography } from '@signozhq/ui/typography';
import styled from 'styled-components';
export const DurationText = styled.div`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,8 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, 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 {
@@ -18,6 +17,13 @@ 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';
@@ -107,19 +113,77 @@ function AlertActionButtons({
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => (): void => setAlertRuleState(undefined), []);
const toggleAlertRule = useCallback(() => {
setIsAlertRuleDisabled((prev) => !prev);
handleAlertStateToggle();
}, [handleAlertStateToggle]);
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;
return (
<>
<div className="alert-action-buttons">
<Tooltip title={isAlertRuleDisabled ? 'Enable alert' : 'Disable alert'}>
{isAlertRuleDisabled !== undefined && (
<Switch onChange={toggleAlertRule} value={!isAlertRuleDisabled} />
)}
</Tooltip>
{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>
)}
<CopyToClipboard textToCopy={window.location.href} />
<Divider type="vertical" className="alert-action-buttons__divider" />
@@ -141,6 +205,14 @@ function AlertActionButtons({
</DropdownMenuSimple>
</div>
<MuteSchedulerDrawer
open={isMuteDrawerOpen}
onClose={(): void => setIsMuteDrawerOpen(false)}
ruleName={ruleDisplayName}
isLoading={isMuting}
onSubmit={mute}
/>
<RenameModal
isOpen={isRenameAlertOpen}
setIsOpen={setIsRenameAlertOpen}

View File

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

View File

@@ -12,6 +12,9 @@ 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';
@@ -43,6 +46,13 @@ 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">
@@ -67,14 +77,23 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
);
return (
<div className="alert-info">
{isV2Alert ? <CreateAlertV2Header /> : CreateAlertV1Header}
<div className="alert-info__action-buttons">
<AlertActionButtons
alertDetails={alertDetails}
ruleId={alertDetails?.id || ''}
/>
<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>
{showMutedBanner && (
<div className="alert-info__banner">
<MutedBanner activeMute={activeMutes[0]} onExpire={refetch} />
</div>
)}
{showDisabledBanner && (
<div className="alert-info__banner">
<DisabledBanner rule={alertDetails as RuletypesRuleDTO} />
</div>
)}
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import { useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { Tabs, TabsProps } from 'antd';
import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon';
@@ -10,10 +9,10 @@ import RoutingPolicies from 'container/RoutingPolicies';
import TriggeredAlerts from 'container/TriggeredAlerts';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
import { CalendarClock, GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
import AlertDetails from 'pages/AlertDetails';
import { AlertListSubTabs, AlertListTabs } from './types';
import { AlertListTabs } from './types';
import './AlertList.styles.scss';
@@ -23,43 +22,9 @@ function AllAlertList(): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const tab = urlQuery.get('tab');
const subTab = urlQuery.get('subTab');
const isAlertHistory = location.pathname === ROUTES.ALERT_HISTORY;
const isAlertOverview = location.pathname === ROUTES.ALERT_OVERVIEW;
const handleConfigurationTabChange = useCallback(
(subTab: string): void => {
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: (
@@ -88,12 +53,22 @@ function AllAlertList(): JSX.Element {
{
label: (
<div className="periscope-tab top-level-tab">
<ConfigureIcon width={14} height={14} />
Configuration
<CalendarClock size={14} />
Planned Downtime
</div>
),
key: AlertListTabs.CONFIGURATION,
children: configurationTab,
key: AlertListTabs.PLANNED_DOWNTIME,
children: <PlannedDowntime />,
},
{
label: (
<div className="periscope-tab top-level-tab">
<ConfigureIcon width={14} height={14} />
Routing Policies
</div>
),
key: AlertListTabs.ROUTING_POLICIES,
children: <RoutingPolicies />,
},
];
@@ -102,21 +77,10 @@ function AllAlertList(): JSX.Element {
destroyInactiveTabPane
items={items}
activeKey={tab || AlertListTabs.ALERT_RULES}
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
onChange={(nextTab): void => {
urlQuery.set('tab', nextTab);
urlQuery.delete('subTab');
urlQuery.delete('search');
safeNavigate(`/alerts?${urlQuery.toString()}`);
}}
className={`alerts-container ${

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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