Compare commits

..

4 Commits

Author SHA1 Message Date
swapnil-signoz
fb26cc36c8 feat: adding integration test for new API endpoint 2026-05-29 18:49:07 +05:30
swapnil-signoz
9ed3486d2c feat: adding get cloud integration service for account handler 2026-05-29 16:20:04 +05:30
Gaurav Tewari
1f406823d8 refactor(frontend): migrate plain antd Input to @signozhq/ui/input (#11401)
* chore: refactor input

* chore: remove markdown

* chore: update markdown

* fix: minior comments for input

* chore: remove comments

---------

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

* chore: filter metric groups

* chore: filter metric groups

* chore: formula correction

* chore: added step flooring note

* chore: comment correction

* chore: comment correction

* chore: removed function

* chore: renamed variables

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

* chore: added todo and note

* chore: added changes based on comments

---------

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

View File

@@ -5945,22 +5945,6 @@ components:
- start
- end
type: object
RuletypesActiveMute:
properties:
description:
type: string
end:
format: date-time
nullable: true
type: string
id:
type: string
name:
type: string
start:
format: date-time
type: string
type: object
RuletypesAlertCompositeQuery:
properties:
panelType:
@@ -6242,11 +6226,6 @@ components:
additionalProperties:
type: string
type: object
mutes:
items:
$ref: '#/components/schemas/RuletypesActiveMute'
nullable: true
type: array
notificationSettings:
$ref: '#/components/schemas/RuletypesNotificationSettings'
preferredChannels:
@@ -7986,6 +7965,80 @@ paths:
tags:
- cloudintegration
/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}/services/{service_id}:
get:
deprecated: false
description: This endpoint gets a service and its configuration for the specified
cloud integration account
operationId: GetAccountService
parameters:
- in: path
name: cloud_provider
required: true
schema:
type: string
- in: path
name: id
required: true
schema:
type: string
- in: path
name: service_id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/CloudintegrationtypesService'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Get service for account
tags:
- cloudintegration
put:
deprecated: false
description: This endpoint updates a service for the specified cloud provider

View File

@@ -31,6 +31,8 @@ import type {
DisconnectAccountPathParameters,
GetAccount200,
GetAccountPathParameters,
GetAccountService200,
GetAccountServicePathParameters,
GetConnectionCredentials200,
GetConnectionCredentialsPathParameters,
GetService200,
@@ -631,6 +633,117 @@ export const useUpdateAccount = <
> => {
return useMutation(getUpdateAccountMutationOptions(options));
};
/**
* This endpoint gets a service and its configuration for the specified cloud integration account
* @summary Get service for account
*/
export const getAccountService = (
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetAccountService200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/accounts/${id}/services/${serviceId}`,
method: 'GET',
signal,
});
};
export const getGetAccountServiceQueryKey = ({
cloudProvider,
id,
serviceId,
}: GetAccountServicePathParameters) => {
return [
`/api/v1/cloud_integrations/${cloudProvider}/accounts/${id}/services/${serviceId}`,
] as const;
};
export const getGetAccountServiceQueryOptions = <
TData = Awaited<ReturnType<typeof getAccountService>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAccountService>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetAccountServiceQueryKey({ cloudProvider, id, serviceId });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getAccountService>>
> = ({ signal }) =>
getAccountService({ cloudProvider, id, serviceId }, signal);
return {
queryKey,
queryFn,
enabled: !!(cloudProvider && id && serviceId),
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getAccountService>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetAccountServiceQueryResult = NonNullable<
Awaited<ReturnType<typeof getAccountService>>
>;
export type GetAccountServiceQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get service for account
*/
export function useGetAccountService<
TData = Awaited<ReturnType<typeof getAccountService>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAccountService>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetAccountServiceQueryOptions(
{ cloudProvider, id, serviceId },
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get service for account
*/
export const invalidateGetAccountService = async (
queryClient: QueryClient,
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetAccountServiceQueryKey({ cloudProvider, id, serviceId }) },
options,
);
return queryClient;
};
/**
* This endpoint updates a service for the specified cloud provider
* @summary Update service

View File

@@ -7051,31 +7051,6 @@ export interface RulestatehistorytypesGettableRuleStateWindowDTO {
state: RuletypesAlertStateDTO;
}
export interface RuletypesActiveMuteDTO {
/**
* @type string
*/
description?: string;
/**
* @type string,null
* @format date-time
*/
end?: string | null;
/**
* @type string
*/
id?: string;
/**
* @type string
*/
name?: string;
/**
* @type string
* @format date-time
*/
start?: string;
}
export enum RuletypesPanelTypeDTO {
value = 'value',
table = 'table',
@@ -7444,10 +7419,6 @@ export interface RuletypesRuleDTO {
* @type object
*/
labels?: RuletypesRuleDTOLabels;
/**
* @type array,null
*/
mutes?: RuletypesActiveMuteDTO[] | null;
notificationSettings?: RuletypesNotificationSettingsDTO;
/**
* @type array
@@ -8595,6 +8566,19 @@ export type UpdateAccountPathParameters = {
cloudProvider: string;
id: string;
};
export type GetAccountServicePathParameters = {
cloudProvider: string;
id: string;
serviceId: string;
};
export type GetAccountService200 = {
data: CloudintegrationtypesServiceDTO;
/**
* @type string
*/
status: string;
};
export type UpdateServicePathParameters = {
cloudProvider: string;
id: string;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -445,6 +445,12 @@ export function PlannedDowntimeForm(
<Form.Item
label="Ends on"
name="endTime"
required={recurrenceType === recurrenceOptions.doesNotRepeat.value}
rules={[
{
required: recurrenceType === recurrenceOptions.doesNotRepeat.value,
},
]}
className={!isEmpty(endTimeText) ? 'formItemWithBullet' : ''}
>
<DatePicker

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
import { Input, Select, Skeleton } from 'antd';
import { Select, Skeleton } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import { Input } from '@signozhq/ui/input';
import cx from 'classnames';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';

View File

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

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { useQueryClient } from 'react-query';
import { Input } from 'antd';
import { Input } from '@signozhq/ui/input';
import SignozModal from 'components/SignozModal/SignozModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useRenameFunnel } from 'hooks/TracesFunnels/useFunnels';

View File

@@ -9,10 +9,10 @@
.ant-input-prefix {
margin-inline-end: 6px;
}
&,
input {
font-family: Inter;
background: var(--l2-background);
font-size: 14px;
line-height: 18px;
font-style: normal;

View File

@@ -1,6 +1,7 @@
import { ChangeEvent } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Button, Input, Popover, Tooltip } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Popover, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { ArrowDownWideNarrow, Check, Plus, Search } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';

View File

@@ -204,5 +204,12 @@
background: var(--l2-background);
height: 38px;
width: 38px;
&:focus:not(:focus-visible),
&.ant-btn:focus:not(:focus-visible) {
border-color: var(--l2-border);
outline: none;
box-shadow: none;
}
}
}

View File

@@ -150,29 +150,17 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
}
func (r *maintenance) DeletePlannedMaintenance(ctx context.Context, id valuer.UUID) error {
return r.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
_, err := r.sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(alertmanagertypes.StorablePlannedMaintenanceRule)).
Where("planned_maintenance_id = ?", id.StringValue()).
Exec(ctx)
if err != nil {
return err
}
_, err := r.sqlstore.
BunDB().
NewDelete().
Model(new(alertmanagertypes.StorablePlannedMaintenance)).
Where("id = ?", id.StringValue()).
Exec(ctx)
if err != nil {
return r.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "cannot delete planned maintenance because it is referenced by associated rules, remove the rules from the planned maintenance first")
}
_, err = r.sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(alertmanagertypes.StorablePlannedMaintenance)).
Where("id = ?", id.StringValue()).
Exec(ctx)
if err != nil {
return err
}
return nil
})
return nil
}
func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance *alertmanagertypes.PostablePlannedMaintenance, id valuer.UUID) error {

View File

@@ -192,6 +192,26 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}/services/{service_id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.GetAccountService),
handler.OpenAPIDef{
ID: "GetAccountService",
Tags: []string{"cloudintegration"},
Summary: "Get service for account",
Description: "This endpoint gets a service and its configuration for the specified cloud integration account",
Request: nil,
RequestContentType: "",
Response: new(citypes.Service),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
// Agent check-in endpoint is kept same as older one to maintain backward compatibility with already deployed agents.
// In the future, this endpoint will be deprecated and a new endpoint will be introduced for consistency with above endpoints.
if err := router.Handle("/api/v1/cloud-integrations/{cloud_provider}/agent-check-in", handler.New(

View File

@@ -76,6 +76,7 @@ type Handler interface {
DisconnectAccount(http.ResponseWriter, *http.Request)
ListServicesMetadata(http.ResponseWriter, *http.Request)
GetService(http.ResponseWriter, *http.Request)
GetAccountService(http.ResponseWriter, *http.Request)
UpdateService(http.ResponseWriter, *http.Request)
AgentCheckIn(http.ResponseWriter, *http.Request)
}

View File

@@ -322,6 +322,51 @@ func (handler *handler) GetService(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusOK, svc)
}
func (handler *handler) GetAccountService(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
provider, err := cloudintegrationtypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
if err != nil {
render.Error(rw, err)
return
}
serviceID, err := cloudintegrationtypes.NewServiceID(provider, mux.Vars(r)["service_id"])
if err != nil {
render.Error(rw, err)
return
}
cloudIntegrationID, err := valuer.NewUUID(mux.Vars(r)["id"])
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
_, err = handler.module.GetConnectedAccount(ctx, orgID, cloudIntegrationID, provider)
if err != nil {
render.Error(rw, err)
return
}
svc, err := handler.module.GetService(ctx, orgID, serviceID, provider, cloudIntegrationID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, svc)
}
func (handler *handler) UpdateService(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()

View File

@@ -149,6 +149,11 @@ func paginateWithBackfill(
groupBy []qbtypes.GroupByKey,
offset, limit int,
) []map[string]string {
// note: we took a stand here that we are NOT removing those metricGroups from the array that are not in metadataMap.
// we are relying on time adjustment logic from alignedMetricWindow. In future if a user complains about seeing metric groups
// with missing metadata, we can consider removing those groups from the metricGroups array here before paginating.
metricKeySet := make(map[string]bool, len(metricGroups))
for _, g := range metricGroups {
metricKeySet[g.compositeKey] = true
@@ -163,7 +168,7 @@ func paginateWithBackfill(
sort.Strings(metadataOnlyKeys)
totalMetric := len(metricGroups)
totalAll := totalMetric + len(metadataOnlyKeys)
totalAll := len(metadataMap)
end := offset + limit
if end > totalAll {
@@ -307,23 +312,57 @@ func parseFullQueryResponse(
return result
}
// buildSamplesTblFingerprintSubQuery returns a SelectBuilder that selects distinct fingerprints
// from the samples table for the given metric names andtime range.
func (m *module) buildSamplesTblFingerprintSubQuery(metricNames []string, startMs, endMs int64) *sqlbuilder.SelectBuilder {
samplesTableName := telemetrymetrics.WhichSamplesTableToUse(
uint64(startMs), uint64(endMs),
metrictypes.UnspecifiedType,
metrictypes.TimeAggregationUnspecified,
nil,
// alignedMetricWindow returns step-floored time bounds and the metric tables
// to use for the given window. The floor matches what the QB v5 metric
// querier does internally (see querybuilder.AdjustedMetricTimeRange).
// Please use the samplesAdjustedStartMs with samples table and tsAdjustedStartMs with ts tables.
// Both can use the same flooredEndMs.
func alignedMetricWindow(startMs, endMs int64) (
uint64, // samplesAdjustedStartMs
uint64, // flooredEndMs
uint64, // tsAdjustedStartMs
string, // distributedTSTable
string, // localTSTable
string, // distributedSamplesTable
string, // localSamplesTable
) {
samplesAdjustedStartMs := uint64(startMs)
flooredEndMs := uint64(endMs)
stepSecs := querybuilder.RecommendedStepIntervalForMetric(samplesAdjustedStartMs, flooredEndMs)
// note: this is the same flooring logic as in querybuilder.AdjustedMetricTimeRange. Duplicated code.
// TODO(nikhilmantri0902): if the querybuilder.AdjustMetricTimeRange logic changes, this needs to be updated too.
if stepSecs > 0 {
samplesAdjustedStartMs = samplesAdjustedStartMs - (samplesAdjustedStartMs % (stepSecs * 1000))
adjustStep := stepSecs
if adjustStep > 60 {
adjustStep = 60
}
flooredEndMs = flooredEndMs - (flooredEndMs % (adjustStep * 1000))
}
tsAdjustedStartMs, _, distributedTSTable, localTSTable := telemetrymetrics.WhichTSTableToUse(
samplesAdjustedStartMs, flooredEndMs, nil,
)
localSamplesTable := strings.TrimPrefix(samplesTableName, "distributed_")
distributedSamplesTable, localSamplesTable := telemetrymetrics.WhichSamplesTableToUse(
samplesAdjustedStartMs, flooredEndMs,
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil,
)
return samplesAdjustedStartMs, flooredEndMs, tsAdjustedStartMs, distributedTSTable, localTSTable, distributedSamplesTable, localSamplesTable
}
// buildSamplesTblFingerprintSubQuery returns a SelectBuilder that selects distinct fingerprints
// from the samples table for the given metric names and time range.
// Bounds must already be step-floored by the caller via alignedMetricWindow.
func (m *module) buildSamplesTblFingerprintSubQuery(metricNames []string, samplesTable string, flooredStart, flooredEnd uint64) *sqlbuilder.SelectBuilder {
fpSB := sqlbuilder.NewSelectBuilder()
fpSB.Select("DISTINCT fingerprint")
fpSB.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, localSamplesTable))
fpSB.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, samplesTable))
fpSB.Where(
fpSB.In("metric_name", sqlbuilder.List(metricNames)),
fpSB.GE("unix_milli", startMs),
fpSB.L("unix_milli", endMs),
fpSB.GE("unix_milli", flooredStart),
fpSB.L("unix_milli", flooredEnd),
)
return fpSB
}
@@ -454,12 +493,12 @@ func (m *module) getMetadata(
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "groupBy must not be empty")
}
// Pick the optimal timeseries table based on time range; also get adjusted start.
adjustedStart, adjustedEnd, distributedTableName, _ := telemetrymetrics.WhichTSTableToUse(
uint64(startMs), uint64(endMs), nil,
)
// Step-floor the window and pick the right tables — matches the bounds the
// QB v5 metric querier uses, so metadataMap covers the same universe the
// ranking sees (see alignedMetricWindow doc).
samplesStartMs, flooredEndMs, tsAdjustedStartMs, distributedTableName, _, _, localSamplesTable := alignedMetricWindow(startMs, endMs)
fpSB := m.buildSamplesTblFingerprintSubQuery(metricNames, startMs, endMs)
fpSB := m.buildSamplesTblFingerprintSubQuery(metricNames, localSamplesTable, samplesStartMs, flooredEndMs)
// Flatten groupBy keys to string names for SQL expressions and result scanning.
groupByCols := make([]string, len(groupBy))
@@ -494,8 +533,8 @@ func (m *module) getMetadata(
innerSB.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, distributedTableName))
innerSB.Where(
innerSB.In("metric_name", sqlbuilder.List(metricNames)),
innerSB.GE("unix_milli", adjustedStart),
innerSB.L("unix_milli", adjustedEnd),
innerSB.GE("unix_milli", tsAdjustedStartMs),
innerSB.LE("unix_milli", flooredEndMs),
fmt.Sprintf("fingerprint IN (%s)", innerSB.Var(fpSB)),
)

View File

@@ -34,9 +34,8 @@ func (m *module) getPerGroupHostStatusCounts(
pageGroupsFilterExpr := buildPageGroupsFilterExpr(pageGroups)
filterExpr := mergeFilterExpressions(reqFilterExpr, pageGroupsFilterExpr)
adjustedStart, adjustedEnd, distributedTimeSeriesTableName, _ := telemetrymetrics.WhichTSTableToUse(
uint64(req.Start), uint64(req.End), nil,
)
// Step-floor bounds + resolve tables in one shot to match QB v5 querier.
samplesStartMs, flooredEndMs, tsAdjustedStartMs, distributedTimeSeriesTableName, _, _, localSamplesTable := alignedMetricWindow(req.Start, req.End)
hostNameExpr := fmt.Sprintf("JSONExtractString(labels, '%s')", inframonitoringtypes.HostNameAttrKey)
@@ -55,15 +54,15 @@ func (m *module) getPerGroupHostStatusCounts(
)
// Build a fingerprint subquery to restrict to fingerprints with actual sample
// data in the original time range (not the wider timeseries table window).
fpSB := m.buildSamplesTblFingerprintSubQuery(metricNames, req.Start, req.End)
// data in the floored time range.
fpSB := m.buildSamplesTblFingerprintSubQuery(metricNames, localSamplesTable, samplesStartMs, flooredEndMs)
sb.Select(selectCols...)
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, distributedTimeSeriesTableName))
sb.Where(
sb.In("metric_name", sqlbuilder.List(metricNames)),
sb.GE("unix_milli", adjustedStart),
sb.L("unix_milli", adjustedEnd),
sb.GE("unix_milli", tsAdjustedStartMs),
sb.LE("unix_milli", flooredEndMs),
fmt.Sprintf("fingerprint IN (%s)", sb.Var(fpSB)),
)

View File

@@ -9,7 +9,6 @@ import (
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/huandu/go-sqlbuilder"
@@ -190,15 +189,9 @@ func (m *module) getPerGroupNodeConditionCounts(
pageGroupsFilterExpr := buildPageGroupsFilterExpr(pageGroups)
mergedFilterExpr := mergeFilterExpressions(userFilterExpr, pageGroupsFilterExpr)
// Resolve tables. Same convention as pods.
adjustedStart, adjustedEnd, _, localTimeSeriesTable := telemetrymetrics.WhichTSTableToUse(
uint64(start), uint64(end), nil,
)
samplesTable := telemetrymetrics.WhichSamplesTableToUse(
uint64(start), uint64(end),
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil,
)
valueCol := telemetrymetrics.ValueColumnForSamplesTable(samplesTable)
// Step-floor bounds + resolve tables in one shot to match QB v5 querier.
samplesStartMs, flooredEndMs, tsAdjustedStartMs, _, localTimeSeriesTable, distributedSamplesTable, _ := alignedMetricWindow(start, end)
valueCol := telemetrymetrics.ValueColumnForSamplesTable(distributedSamplesTable)
// ----- timeSeriesFPs -----
timeSeriesFPs := sqlbuilder.NewSelectBuilder()
@@ -215,8 +208,8 @@ func (m *module) getPerGroupNodeConditionCounts(
timeSeriesFPs.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, localTimeSeriesTable))
timeSeriesFPs.Where(
timeSeriesFPs.E("metric_name", nodeConditionMetricName),
timeSeriesFPs.GE("unix_milli", adjustedStart),
timeSeriesFPs.L("unix_milli", adjustedEnd),
timeSeriesFPs.GE("unix_milli", tsAdjustedStartMs),
timeSeriesFPs.LE("unix_milli", flooredEndMs),
)
if mergedFilterExpr != "" {
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
@@ -249,12 +242,12 @@ func (m *module) getPerGroupNodeConditionCounts(
latestConditionPerNode.Select(latestConditionPerNodeSelectCols...)
latestConditionPerNode.From(fmt.Sprintf(
"%s.%s AS samples INNER JOIN time_series_fps AS tsfp ON samples.fingerprint = tsfp.fingerprint",
telemetrymetrics.DBName, samplesTable,
telemetrymetrics.DBName, distributedSamplesTable,
))
latestConditionPerNode.Where(
latestConditionPerNode.E("samples.metric_name", nodeConditionMetricName),
latestConditionPerNode.GE("samples.unix_milli", start),
latestConditionPerNode.L("samples.unix_milli", end),
latestConditionPerNode.GE("samples.unix_milli", samplesStartMs),
latestConditionPerNode.L("samples.unix_milli", flooredEndMs),
"tsfp.node_name != ''",
)
latestConditionPerNode.GroupBy(latestConditionPerNodeGroupBy...)

View File

@@ -10,7 +10,6 @@ import (
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/huandu/go-sqlbuilder"
@@ -96,14 +95,16 @@ func buildPodRecords(
}
}
if attrs, ok := metadataMap[compositeKey]; ok && isPodUIDInGroupBy {
// the condition above ensures we deduce age only if pod uid is in group by because if
// it's not in group by then we might have multiple pod uids in the same group and hence then podAge wont make sense
if startTimeStr, exists := attrs[podStartTimeAttrKey]; exists && startTimeStr != "" {
if t, err := time.Parse(time.RFC3339, startTimeStr); err == nil {
startTimeMs := t.UnixMilli()
if startTimeMs > 0 {
record.PodAge = reqEnd - startTimeMs
if attrs, ok := metadataMap[compositeKey]; ok {
// podAge only makes sense when pod uid is in groupBy. Otherwise the
// group can contain multiple pods with different start times.
if isPodUIDInGroupBy {
if startTimeStr, exists := attrs[podStartTimeAttrKey]; exists && startTimeStr != "" {
if t, err := time.Parse(time.RFC3339, startTimeStr); err == nil {
startTimeMs := t.UnixMilli()
if startTimeMs > 0 {
record.PodAge = reqEnd - startTimeMs
}
}
}
}
@@ -209,15 +210,9 @@ func (m *module) getPerGroupPodPhaseCounts(
pageGroupsFilterExpr := buildPageGroupsFilterExpr(pageGroups)
mergedFilterExpr := mergeFilterExpressions(userFilterExpr, pageGroupsFilterExpr)
// Resolve tables. Same convention as hosts (distributed names from helpers).
adjustedStart, adjustedEnd, _, localTimeSeriesTable := telemetrymetrics.WhichTSTableToUse(
uint64(start), uint64(end), nil,
)
samplesTable := telemetrymetrics.WhichSamplesTableToUse(
uint64(start), uint64(end),
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil,
)
valueCol := telemetrymetrics.ValueColumnForSamplesTable(samplesTable)
// Step-floor bounds + resolve tables in one shot to match QB v5 querier.
samplesStartMs, flooredEndMs, tsAdjustedStart, _, localTimeSeriesTable, distributedSamplesTable, _ := alignedMetricWindow(start, end)
valueCol := telemetrymetrics.ValueColumnForSamplesTable(distributedSamplesTable)
// ----- timeSeriesFPs -----
timeSeriesFPs := sqlbuilder.NewSelectBuilder()
@@ -234,8 +229,8 @@ func (m *module) getPerGroupPodPhaseCounts(
timeSeriesFPs.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, localTimeSeriesTable))
timeSeriesFPs.Where(
timeSeriesFPs.E("metric_name", podPhaseMetricName),
timeSeriesFPs.GE("unix_milli", adjustedStart),
timeSeriesFPs.L("unix_milli", adjustedEnd),
timeSeriesFPs.GE("unix_milli", tsAdjustedStart),
timeSeriesFPs.LE("unix_milli", flooredEndMs),
)
if mergedFilterExpr != "" {
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
@@ -267,12 +262,12 @@ func (m *module) getPerGroupPodPhaseCounts(
latestPhasePerPod.Select(latestPhasePerPodSelectCols...)
latestPhasePerPod.From(fmt.Sprintf(
"%s.%s AS samples INNER JOIN time_series_fps AS tsfp ON samples.fingerprint = tsfp.fingerprint",
telemetrymetrics.DBName, samplesTable,
telemetrymetrics.DBName, distributedSamplesTable,
))
latestPhasePerPod.Where(
latestPhasePerPod.E("samples.metric_name", podPhaseMetricName),
latestPhasePerPod.GE("samples.unix_milli", start),
latestPhasePerPod.L("samples.unix_milli", end),
latestPhasePerPod.GE("samples.unix_milli", samplesStartMs),
latestPhasePerPod.L("samples.unix_milli", flooredEndMs),
"tsfp.pod_uid != ''",
)
latestPhasePerPod.GroupBy(latestPhasePerPodGroupBy...)

View File

@@ -981,7 +981,7 @@ func (m *module) fetchMetricsStatsWithSamples(
ctx = m.withMetricsExplorerContext(ctx, "fetchMetricsStatsWithSamples")
start, end, distributedTsTable, localTsTable := telemetrymetrics.WhichTSTableToUse(uint64(req.Start), uint64(req.End), nil)
samplesTable := telemetrymetrics.WhichSamplesTableToUse(uint64(req.Start), uint64(req.End), metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil)
samplesTable, _ := telemetrymetrics.WhichSamplesTableToUse(uint64(req.Start), uint64(req.End), metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil)
countExp := telemetrymetrics.CountExpressionForSamplesTable(samplesTable)
// Timeseries counts per metric
@@ -1155,7 +1155,7 @@ func (m *module) computeSamplesTreemap(ctx context.Context, req *metricsexplorer
ctx = m.withMetricsExplorerContext(ctx, "computeSamplesTreemap")
start, end, distributedTsTable, localTsTable := telemetrymetrics.WhichTSTableToUse(uint64(req.Start), uint64(req.End), nil)
samplesTable := telemetrymetrics.WhichSamplesTableToUse(uint64(req.Start), uint64(req.End), metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil)
samplesTable, _ := telemetrymetrics.WhichSamplesTableToUse(uint64(req.Start), uint64(req.End), metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil)
countExp := telemetrymetrics.CountExpressionForSamplesTable(samplesTable)
candidateLimit := req.Limit + 50

View File

@@ -29,23 +29,15 @@ func (handler *handler) ListRules(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
rules, err := handler.ruler.ListRuleStates(ctx)
if err != nil {
render.Error(rw, err)
return
}
schedules, _ := handler.ruler.MaintenanceStore().ListPlannedMaintenance(ctx, claims.OrgID)
view := make([]*ruletypes.Rule, 0, len(rules.Rules))
for _, rule := range rules.Rules {
view = append(view, ruletypes.NewRule(rule, schedules))
view = append(view, ruletypes.NewRule(rule))
}
render.Success(rw, http.StatusOK, view)
@@ -55,12 +47,6 @@ func (handler *handler) GetRuleByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
id, err := valuer.NewUUID(mux.Vars(req)["id"])
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
@@ -73,9 +59,7 @@ func (handler *handler) GetRuleByID(rw http.ResponseWriter, req *http.Request) {
return
}
schedules, _ := handler.ruler.MaintenanceStore().ListPlannedMaintenance(ctx, claims.OrgID)
render.Success(rw, http.StatusOK, ruletypes.NewRule(rule, schedules))
render.Success(rw, http.StatusOK, ruletypes.NewRule(rule))
}
func (handler *handler) CreateRule(rw http.ResponseWriter, req *http.Request) {
@@ -95,7 +79,7 @@ func (handler *handler) CreateRule(rw http.ResponseWriter, req *http.Request) {
return
}
render.Success(rw, http.StatusCreated, ruletypes.NewRule(rule, nil))
render.Success(rw, http.StatusCreated, ruletypes.NewRule(rule))
}
func (handler *handler) UpdateRuleByID(rw http.ResponseWriter, req *http.Request) {
@@ -166,7 +150,7 @@ func (handler *handler) PatchRuleByID(rw http.ResponseWriter, req *http.Request)
return
}
render.Success(rw, http.StatusOK, ruletypes.NewRule(rule, nil))
render.Success(rw, http.StatusOK, ruletypes.NewRule(rule))
}
func (handler *handler) TestRule(rw http.ResponseWriter, req *http.Request) {

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