Compare commits

..

15 Commits

Author SHA1 Message Date
Jatinderjit Singh
a7ecbb8fac fix: tests to use UTC instead of utc 2026-06-08 20:21:49 +05:30
Jatinderjit Singh
84d678a268 Merge branch 'main' into refactor/planned-maintenance 2026-06-08 11:20:33 +05:30
Jatinderjit Singh
2c635f2892 fix: don't let one corrupt maintenance break the list
ListPlannedMaintenance now reads the schedule as raw text and parses each
row individually, skipping and logging the bad ones so the rest survive.

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 17:21:00 +05:30
230 changed files with 5447 additions and 6708 deletions

View File

@@ -39,7 +39,6 @@ jobs:
matrix:
suite:
- alerts
- basepath
- callbackauthn
- cloudintegrations
- dashboard
@@ -84,7 +83,7 @@ jobs:
run: |
cd tests && uv sync
- name: webdriver
if: matrix.suite == 'callbackauthn' || matrix.suite == 'basepath'
if: matrix.suite == 'callbackauthn'
run: |
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee -a /etc/apt/sources.list.d/google-chrome.list

View File

@@ -91,7 +91,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
sqlstoreProviderFactories(),
signoz.NewTelemetryStoreProviderFactories(),
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
return signoz.NewAuthNs(ctx, providerSettings, store, licensing, config.Global)
return signoz.NewAuthNs(ctx, providerSettings, store, licensing)
},
func(ctx context.Context, sqlstore sqlstore.SQLStore, config authz.Config, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore, config)

View File

@@ -107,17 +107,17 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
sqlstoreProviderFactories(),
signoz.NewTelemetryStoreProviderFactories(),
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
samlCallbackAuthN, err := samlcallbackauthn.New(ctx, store, licensing, config.Global)
samlCallbackAuthN, err := samlcallbackauthn.New(ctx, store, licensing)
if err != nil {
return nil, err
}
oidcCallbackAuthN, err := oidccallbackauthn.New(store, licensing, providerSettings, config.Global)
oidcCallbackAuthN, err := oidccallbackauthn.New(store, licensing, providerSettings)
if err != nil {
return nil, err
}
authNs, err := signoz.NewAuthNs(ctx, providerSettings, store, licensing, config.Global)
authNs, err := signoz.NewAuthNs(ctx, providerSettings, store, licensing)
if err != nil {
return nil, err
}

View File

@@ -409,10 +409,6 @@ components:
properties:
duration:
type: string
endTime:
format: date-time
nullable: true
type: string
repeatOn:
items:
$ref: '#/components/schemas/AlertmanagertypesRepeatOn'
@@ -420,11 +416,7 @@ components:
type: array
repeatType:
$ref: '#/components/schemas/AlertmanagertypesRepeatType'
startTime:
format: date-time
type: string
required:
- startTime
- duration
- repeatType
type: object
@@ -458,6 +450,7 @@ components:
type: string
required:
- timezone
- startTime
type: object
AuthtypesAttributeMapping:
properties:

View File

@@ -5,12 +5,10 @@ import (
"fmt"
"log/slog"
"net/url"
"path"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/client"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/authtypes"
@@ -28,14 +26,13 @@ var defaultScopes []string = []string{"email", "profile", oidc.ScopeOpenID}
var _ authn.CallbackAuthN = (*AuthN)(nil)
type AuthN struct {
settings factory.ScopedProviderSettings
store authtypes.AuthNStore
licensing licensing.Licensing
httpClient *client.Client
globalConfig global.Config
settings factory.ScopedProviderSettings
store authtypes.AuthNStore
licensing licensing.Licensing
httpClient *client.Client
}
func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSettings factory.ProviderSettings, globalConfig global.Config) (*AuthN, error) {
func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSettings factory.ProviderSettings) (*AuthN, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn")
httpClient, err := client.New(providerSettings.Logger, providerSettings.TracerProvider, providerSettings.MeterProvider)
@@ -44,11 +41,10 @@ func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSett
}
return &AuthN{
settings: settings,
store: store,
licensing: licensing,
httpClient: httpClient,
globalConfig: globalConfig,
settings: settings,
store: store,
licensing: licensing,
httpClient: httpClient,
}, nil
}
@@ -201,7 +197,7 @@ func (a *AuthN) oidcProviderAndoauth2Config(ctx context.Context, siteURL *url.UR
RedirectURL: (&url.URL{
Scheme: siteURL.Scheme,
Host: siteURL.Host,
Path: path.Join(a.globalConfig.ExternalPath(), redirectPath),
Path: redirectPath,
}).String(),
}, nil
}

View File

@@ -6,12 +6,10 @@ import (
"encoding/base64"
"encoding/pem"
"net/url"
"path"
"strings"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -26,16 +24,14 @@ const (
var _ authn.CallbackAuthN = (*AuthN)(nil)
type AuthN struct {
store authtypes.AuthNStore
licensing licensing.Licensing
globalConfig global.Config
store authtypes.AuthNStore
licensing licensing.Licensing
}
func New(ctx context.Context, store authtypes.AuthNStore, licensing licensing.Licensing, globalConfig global.Config) (*AuthN, error) {
func New(ctx context.Context, store authtypes.AuthNStore, licensing licensing.Licensing) (*AuthN, error) {
return &AuthN{
store: store,
licensing: licensing,
globalConfig: globalConfig,
store: store,
licensing: licensing,
}, nil
}
@@ -136,7 +132,7 @@ func (a *AuthN) serviceProvider(siteURL *url.URL, authDomain *authtypes.AuthDoma
return nil, err
}
acsURL := &url.URL{Scheme: siteURL.Scheme, Host: siteURL.Host, Path: path.Join(a.globalConfig.ExternalPath(), redirectPath)}
acsURL := &url.URL{Scheme: siteURL.Scheme, Host: siteURL.Host, Path: redirectPath}
// Note:
// The ServiceProviderIssuer is the client id in case of keycloak. Since we set it to the host here, we need to set the client id == host in keycloak.

View File

@@ -413,21 +413,11 @@ export interface AlertmanagertypesRecurrenceDTO {
* @type string
*/
duration: string;
/**
* @type string,null
* @format date-time
*/
endTime?: string | null;
/**
* @type array,null
*/
repeatOn?: AlertmanagertypesRepeatOnDTO[] | null;
repeatType: AlertmanagertypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: string;
}
export interface AlertmanagertypesScheduleDTO {
@@ -441,7 +431,7 @@ export interface AlertmanagertypesScheduleDTO {
* @type string
* @format date-time
*/
startTime?: string;
startTime: string;
/**
* @type string
*/

View File

@@ -1,5 +1,5 @@
.alertHistory {
.alert-history {
display: flex;
flex-direction: column;
gap: var(--spacing-10);
gap: 24px;
}

View File

@@ -3,13 +3,13 @@ import { useState } from 'react';
import Statistics from './Statistics/Statistics';
import Timeline from './Timeline/Timeline';
import styles from './AlertHistory.module.scss';
import './AlertHistory.styles.scss';
function AlertHistory(): JSX.Element {
const [totalCurrentTriggers, setTotalCurrentTriggers] = useState(0);
return (
<div className={styles.alertHistory}>
<div className="alert-history">
<Statistics
totalCurrentTriggers={totalCurrentTriggers}
setTotalCurrentTriggers={setTotalCurrentTriggers}

View File

@@ -1,40 +0,0 @@
.alertPopoverTriggerAction {
cursor: pointer;
}
.alertHistoryPopover {
:global(.ant-popover-inner) {
border: 1px solid var(--l1-border);
background: var(--l1-background) !important;
padding: 0 !important;
}
:global(.ant-popover-arrow) {
&::before {
background: var(--l1-background);
}
}
}
.contributorRowPopoverButtons {
display: flex;
flex-direction: column;
}
.contributorRowPopoverButtonsButton {
display: flex;
align-items: center;
gap: var(--spacing-3);
padding: var(--spacing-6) var(--spacing-7);
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
letter-spacing: 0.14px;
width: 160px;
cursor: pointer;
background: var(--l1-background);
border-color: var(--l1-border);
&:hover {
background: var(--l2-background);
}
}

View File

@@ -0,0 +1,15 @@
.alert-popover-trigger-action {
cursor: pointer;
}
.alert-history-popover {
.ant-popover-inner {
border: 1px solid var(--l1-border);
background: var(--l1-background) !important;
}
.ant-popover-arrow {
&::before {
background: var(--l1-background);
}
}
}

View File

@@ -7,7 +7,7 @@ import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { DraftingCompass } from '@signozhq/icons';
import styles from './AlertPopover.module.scss';
import './AlertPopover.styles.scss';
type Props = {
children: React.ReactNode;
@@ -24,30 +24,30 @@ function PopoverContent({
}): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<div className={styles.contributorRowPopoverButtons}>
<div className="contributor-row-popover-buttons">
{!!relatedLogsLink && (
<Link
to={`${ROUTES.LOGS_EXPLORER}?${relatedLogsLink}`}
className={styles.contributorRowPopoverButtonsButton}
className="contributor-row-popover-buttons__button"
>
<div>
<div className="icon">
<LogsIcon />
</div>
<div>View Logs</div>
<div className="text">View Logs</div>
</Link>
)}
{!!relatedTracesLink && (
<Link
to={`${ROUTES.TRACES_EXPLORER}?${relatedTracesLink}`}
className={styles.contributorRowPopoverButtonsButton}
className="contributor-row-popover-buttons__button"
>
<div>
<div className="icon">
<DraftingCompass
size={14}
color={isDarkMode ? Color.BG_VANILLA_400 : Color.TEXT_INK_400}
/>
</div>
<div>View Traces</div>
<div className="text">View Traces</div>
</Link>
)}
</div>
@@ -64,13 +64,13 @@ function AlertPopover({
relatedLogsLink,
}: Props): JSX.Element {
return (
<div className={styles.alertPopoverTriggerAction}>
<div className="alert-popover-trigger-action">
<Popover
showArrow={false}
placement="bottom"
color="linear-gradient(139deg, rgba(18, 19, 23, 1) 0%, rgba(18, 19, 23, 1) 98.68%)"
destroyTooltipOnHide
rootClassName={styles.alertHistoryPopover}
rootClassName="alert-history-popover"
content={
<PopoverContent
relatedTracesLink={relatedTracesLink}
@@ -112,3 +112,4 @@ export function ConditionalAlertPopover({
}
return <div>{children}</div>;
}
export default AlertPopover;

View File

@@ -4,5 +4,5 @@
height: 280px;
border: 1px solid var(--l1-border);
border-radius: 4px;
margin: 0 var(--spacing-8);
margin: 0 16px;
}

View File

@@ -3,7 +3,7 @@ import { AlertRuleStats } from 'types/api/alerts/def';
import StatsCardsRenderer from './StatsCardsRenderer/StatsCardsRenderer';
import TopContributorsRenderer from './TopContributorsRenderer/TopContributorsRenderer';
import styles from './Statistics.module.scss';
import './Statistics.styles.scss';
function Statistics({
setTotalCurrentTriggers,
@@ -13,7 +13,7 @@ function Statistics({
totalCurrentTriggers: AlertRuleStats['totalCurrentTriggers'];
}): JSX.Element {
return (
<div className={styles.statistics}>
<div className="statistics">
<StatsCardsRenderer setTotalCurrentTriggers={setTotalCurrentTriggers} />
<TopContributorsRenderer totalCurrentTriggers={totalCurrentTriggers} />
</div>

View File

@@ -1,102 +0,0 @@
.statsCard {
width: 21.7%;
border-right: 1px solid var(--l1-border);
padding: var(--spacing-4) var(--spacing-6) var(--spacing-6);
}
.statsCardEmpty {
justify-content: normal;
}
.statsCardTitleWrapper {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
text-transform: uppercase;
font-size: var(--periscope-font-size-base);
line-height: 22px;
color: var(--l2-foreground);
font-weight: var(--font-weight-medium);
}
.durationIndicator {
display: flex;
align-items: center;
gap: var(--spacing-2);
}
.icon {
display: flex;
align-self: center;
}
.text {
text-transform: uppercase;
color: var(--l3-foreground);
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-semibold);
letter-spacing: 0.48px;
}
.statsCardStats {
margin-top: var(--spacing-10);
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
.countLabel {
color: var(--l1-foreground);
font-family: var(--periscope-font-family-mono);
font-size: var(--font-size-2xl);
line-height: 36px;
}
.statsCardAlertHistoryGraph {
margin-top: var(--spacing-16);
}
.alertHistoryGraph {
width: 100%;
height: 72px;
}
.changePercentage {
width: max-content;
display: flex;
padding: var(--spacing-2) var(--spacing-4);
border-radius: 20px;
align-items: center;
gap: var(--spacing-2);
}
// TODO(@signozhq/design-tokens): replace --text-forest-* with --success-foreground after release
.changePercentageSuccess {
background: color-mix(in srgb, var(--text-forest-500) 10%, transparent);
color: var(--text-forest-400);
}
.changePercentageError {
background: color-mix(in srgb, var(--danger-background) 10%, transparent);
color: var(--danger-foreground);
}
.changePercentageNoPreviousData {
color: var(--primary-foreground);
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
padding: var(--spacing-2) var(--spacing-8);
}
.changePercentageIcon {
display: flex;
align-self: center;
}
.changePercentageLabel {
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-medium);
line-height: 16px;
}

View File

@@ -0,0 +1,94 @@
.stats-card {
width: 21.7%;
border-right: 1px solid var(--l1-border);
padding: 9px 12px 13px;
&--empty {
justify-content: normal;
}
&__title-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
.title {
text-transform: uppercase;
font-size: 13px;
line-height: 22px;
color: var(--l2-foreground);
font-weight: 500;
}
.duration-indicator {
display: flex;
align-items: center;
gap: 4px;
.icon {
display: flex;
align-self: center;
}
.text {
text-transform: uppercase;
color: var(--l3-foreground);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.48px;
}
}
}
&__stats {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 4px;
.count-label {
color: var(--l1-foreground);
font-family: 'Geist Mono';
font-size: 24px;
line-height: 36px;
}
}
&__alert-history-graph {
margin-top: 80px;
.alert-history-graph {
width: 100%;
height: 72px;
}
}
}
.change-percentage {
width: max-content;
display: flex;
padding: 4px 8px;
border-radius: 20px;
align-items: center;
gap: 4px;
&--success {
background: color-mix(in srgb, var(--text-forest-500) 10%, transparent);
color: var(--text-forest-400);
}
&--error {
background: color-mix(in srgb, var(--error-background) 10%, transparent);
color: var(--error-foreground);
}
&--no-previous-data {
color: var(--primary-foreground);
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
padding: 4px 16px;
}
&__icon {
display: flex;
align-self: center;
}
&__label {
font-size: 12px;
font-weight: 500;
line-height: 16px;
}
}

View File

@@ -1,4 +1,3 @@
import cx from 'classnames';
import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import { QueryParams } from 'constants/query';
@@ -13,7 +12,7 @@ import {
extractDayFromTimestamp,
} from './utils';
import styles from './StatsCard.module.scss';
import './StatsCard.styles.scss';
type ChangePercentageProps = {
percentage: number;
@@ -27,11 +26,11 @@ function ChangePercentage({
}: ChangePercentageProps): JSX.Element {
if (direction > 0) {
return (
<div className={cx(styles.changePercentage, styles.changePercentageSuccess)}>
<div className={styles.changePercentageIcon}>
<div className="change-percentage change-percentage--success">
<div className="change-percentage__icon">
<ArrowDownLeft size={14} color={Color.BG_FOREST_500} />
</div>
<div className={styles.changePercentageLabel}>
<div className="change-percentage__label">
{percentage}% vs Last {duration}
</div>
</div>
@@ -39,11 +38,11 @@ function ChangePercentage({
}
if (direction < 0) {
return (
<div className={cx(styles.changePercentage, styles.changePercentageError)}>
<div className={styles.changePercentageIcon}>
<div className="change-percentage change-percentage--error">
<div className="change-percentage__icon">
<ArrowUpRight size={14} color={Color.BG_CHERRY_500} />
</div>
<div className={styles.changePercentageLabel}>
<div className="change-percentage__label">
{percentage}% vs Last {duration}
</div>
</div>
@@ -51,13 +50,8 @@ function ChangePercentage({
}
return (
<div
className={cx(
styles.changePercentage,
styles.changePercentageNoPreviousData,
)}
>
<div className={styles.changePercentageLabel}>no previous data</div>
<div className="change-percentage change-percentage--no-previous-data">
<div className="change-percentage__label">no previous data</div>
</div>
);
}
@@ -109,27 +103,27 @@ function StatsCard({
const formattedEndTimeForTooltip = convertTimestampToLocaleDateString(endTime);
return (
<div className={cx(styles.statsCard, { [styles.statsCardEmpty]: isEmpty })}>
<div className={styles.statsCardTitleWrapper}>
<div className={styles.title}>{title}</div>
<div className={styles.durationIndicator}>
<div className={styles.icon}>
<div className={`stats-card ${isEmpty ? 'stats-card--empty' : ''}`}>
<div className="stats-card__title-wrapper">
<div className="title">{title}</div>
<div className="duration-indicator">
<div className="icon">
<Calendar size={14} color={Color.BG_SLATE_200} />
</div>
{relativeTime ? (
<div className={styles.text}>{displayTime}</div>
<div className="text">{displayTime}</div>
) : (
<Tooltip
title={`From ${formattedStartTimeForTooltip} to ${formattedEndTimeForTooltip}`}
>
<div className={styles.text}>{displayTime}</div>
<div className="text">{displayTime}</div>
</Tooltip>
)}
</div>
</div>
<div className={styles.statsCardStats}>
<div className={styles.countLabel}>
<div className="stats-card__stats">
<div className="count-label">
{isEmpty ? emptyMessage : displayValue || totalCurrentCount}
</div>
@@ -140,8 +134,8 @@ function StatsCard({
/>
</div>
<div className={styles.statsCardAlertHistoryGraph}>
<div className={styles.alertHistoryGraph}>
<div className="stats-card__alert-history-graph">
<div className="alert-history-graph">
{!isEmpty && timeSeries.length > 1 && (
<StatsGraph timeSeries={timeSeries} changeDirection={changeDirection} />
)}

View File

@@ -1,45 +0,0 @@
.topContributorsCard {
width: 56.6%;
overflow: hidden;
}
.topContributorsCardHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-4) var(--spacing-6);
border-bottom: 1px solid var(--l1-border);
}
.title {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-medium);
line-height: 22px;
letter-spacing: 0.52px;
text-transform: uppercase;
}
.viewAll {
display: flex;
align-items: center;
gap: var(--spacing-2);
cursor: pointer;
padding: 0;
height: var(--line-height-20);
&:hover {
background-color: transparent !important;
}
}
.label {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}
.icon {
display: flex;
}

View File

@@ -0,0 +1,163 @@
.top-contributors-card {
width: 56.6%;
overflow: hidden;
&--view-all {
width: auto;
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--l1-border);
.title {
color: var(--l2-foreground);
font-size: 13px;
font-weight: 500;
line-height: 22px;
letter-spacing: 0.52px;
text-transform: uppercase;
}
.view-all {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
padding: 0;
height: 20px;
&:hover {
background-color: transparent !important;
}
.label {
color: var(--l2-foreground);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}
.icon {
display: flex;
}
}
}
.contributors-row {
height: 80px;
}
.top-contributors-progress {
--progress-background: transparent;
}
&__content {
.ant-table {
&-cell {
padding: 12px !important;
}
}
.contributors-row {
background: var(--l1-background);
td {
border: none !important;
}
&:not(:last-of-type) td {
border-bottom: 1px solid var(--l1-border) !important;
}
}
.total-contribution {
color: var(--primary-foreground);
font-family: 'Geist Mono';
font-size: 12px;
font-weight: 500;
letter-spacing: -0.06px;
padding: 4px 8px;
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
border-radius: 50px;
width: max-content;
}
}
.empty-content {
margin: 16px 12px;
padding: 40px 45px;
display: flex;
flex-direction: column;
gap: 12px;
border: 1px dashed var(--l1-border);
border-radius: 6px;
&__icon {
font-family: Inter;
font-size: 20px;
line-height: 26px;
letter-spacing: -0.103px;
}
&__text {
color: var(--l2-foreground);
line-height: 18px;
.bold-text {
color: var(--l1-foreground);
font-weight: 500;
}
}
&__button-wrapper {
margin-top: 12px;
.configure-alert-rule-button {
padding: 8px 16px;
border-radius: 2px;
background: var(--l3-background);
border-width: 0;
color: var(--l1-foreground);
line-height: 24px;
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
}
}
}
}
.ant-popover-inner:has(.contributor-row-popover-buttons) {
padding: 0 !important;
}
.contributor-row-popover-buttons {
display: flex;
flex-direction: column;
&__button {
display: flex;
align-items: center;
gap: 6px;
padding: 12px 15px;
color: var(--l2-foreground);
font-size: 14px;
letter-spacing: 0.14px;
width: 160px;
cursor: pointer;
background: var(--l1-background);
border-color: var(--l1-border);
.text,
.icon {
color: var(--l1-foreground);
}
&:hover {
background: var(--l2-background);
.text,
.icon {
color: var(--l1-foreground);
}
}
.icon {
display: flex;
}
}
}
.view-all-drawer {
border-radius: 4px;
}

View File

@@ -10,7 +10,7 @@ import TopContributorsContent from './TopContributorsContent';
import { TopContributorsCardProps } from './types';
import ViewAllDrawer from './ViewAllDrawer';
import styles from './TopContributorsCard.module.scss';
import './TopContributorsCard.styles.scss';
function TopContributorsCard({
topContributorsData,
@@ -48,17 +48,13 @@ function TopContributorsCard({
return (
<>
<div className={styles.topContributorsCard}>
<div className={styles.topContributorsCardHeader}>
<div className={styles.title}>top contributors</div>
<div className="top-contributors-card">
<div className="top-contributors-card__header">
<div className="title">top contributors</div>
{topContributorsData.length > 3 && (
<Button
type="text"
className={styles.viewAll}
onClick={toggleViewAllDrawer}
>
<div className={styles.label}>View all</div>
<div className={styles.icon}>
<Button type="text" className="view-all" onClick={toggleViewAllDrawer}>
<div className="label">View all</div>
<div className="icon">
<ArrowRight
size={14}
color={isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}

View File

@@ -1,27 +0,0 @@
.emptyContent {
margin: var(--spacing-8) var(--spacing-6);
padding: var(--spacing-20) 45px;
display: flex;
flex-direction: column;
gap: var(--spacing-6);
border: 1px dashed var(--l1-border);
border-radius: 6px;
}
.emptyContentIcon {
font-family: var(--font-family-inter);
font-size: var(--font-size-xl);
line-height: 26px;
letter-spacing: -0.103px;
}
.emptyContentText {
color: var(--l2-foreground);
line-height: var(--line-height-18);
}
.topContributorsCardContent {
:global(.ant-table-cell) {
padding: var(--spacing-6) !important;
}
}

View File

@@ -1,4 +1,3 @@
import styles from './TopContributorsContent.module.scss';
import TopContributorsRows from './TopContributorsRows';
import { TopContributorsCardProps } from './types';
@@ -10,9 +9,9 @@ function TopContributorsContent({
if (isEmpty) {
return (
<div className={styles.emptyContent}>
<div className={styles.emptyContentIcon}></div>
<div className={styles.emptyContentText}>
<div className="empty-content">
<div className="empty-content__icon"></div>
<div className="empty-content__text">
Top contributors highlight the most frequently triggering group-by
attributes in multi-dimensional alerts
</div>
@@ -21,7 +20,7 @@ function TopContributorsContent({
}
return (
<div className={styles.topContributorsCardContent}>
<div className="top-contributors-card__content">
<TopContributorsRows
topContributors={topContributorsData.slice(0, 3)}
totalCurrentTriggers={totalCurrentTriggers}

View File

@@ -1,28 +0,0 @@
.contributorsRow {
height: 80px;
background: var(--l1-background);
td {
border: none !important;
}
&:not(:last-of-type) td {
border-bottom: 1px solid var(--l1-border) !important;
}
}
.topContributorsProgress {
--progress-background: transparent;
}
.totalContribution {
color: var(--primary-foreground);
font-family: var(--periscope-font-family-mono);
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-medium);
letter-spacing: -0.06px;
padding: var(--spacing-2) var(--spacing-4);
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
border-radius: 50px;
width: max-content;
}

View File

@@ -12,8 +12,6 @@ import {
AlertRuleTopContributors,
} from 'types/api/alerts/def';
import styles from './TopContributorsRows.module.scss';
function TopContributorsRows({
topContributors,
totalCurrentTriggers,
@@ -55,7 +53,7 @@ function TopContributorsRows({
percent={(count / totalCurrentTriggers) * 100}
showInfo={false}
strokeColor={Color.BG_ROBIN_500}
className={styles.topContributorsProgress}
className="top-contributors-progress"
/>
</ConditionalAlertPopover>
),
@@ -70,7 +68,7 @@ function TopContributorsRows({
relatedTracesLink={record.relatedTracesLink}
relatedLogsLink={record.relatedLogsLink}
>
<div className={styles.totalContribution}>
<div className="total-contribution">
{count}/{totalCurrentTriggers}
</div>
</ConditionalAlertPopover>
@@ -90,7 +88,7 @@ function TopContributorsRows({
return (
<Table
rowClassName={styles.contributorsRow}
rowClassName="contributors-row"
rowKey={(row): string => `top-contributor-${row.fingerprint}`}
onRow={handleRowClick}
columns={columns}

View File

@@ -1,13 +0,0 @@
.topContributorsCardViewAll {
width: auto;
}
.topContributorsCardContent {
:global(.ant-table-cell) {
padding: var(--spacing-6) !important;
}
}
.viewAllDrawer {
border-radius: 4px;
}

View File

@@ -3,7 +3,6 @@ import { Drawer } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { AlertRuleStats, AlertRuleTopContributors } from 'types/api/alerts/def';
import styles from './ViewAllDrawer.module.scss';
import TopContributorsRows from './TopContributorsRows';
function ViewAllDrawer({
@@ -25,15 +24,15 @@ function ViewAllDrawer({
onClose={toggleViewAllDrawer}
placement="right"
width="50%"
className={styles.viewAllDrawer}
className="view-all-drawer"
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
title="Viewing All Contributors"
>
<div className={styles.topContributorsCardViewAll}>
<div className={styles.topContributorsCardContent}>
<div className="top-contributors-card--view-all">
<div className="top-contributors-card__content">
<TopContributorsRows
topContributors={topContributorsData}
totalCurrentTriggers={totalCurrentTriggers}

View File

@@ -0,0 +1,35 @@
.timeline-graph {
display: flex;
flex-direction: column;
gap: 24px;
background: var(--l2-background);
padding: 12px;
border-radius: 4px;
border: 1px solid var(--l1-border);
height: 150px;
&__title {
width: max-content;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
}
&__chart {
.chart-placeholder {
width: 100%;
height: 52px;
background: color-mix(in srgb, var(--l1-foreground) 12%, transparent);
display: flex;
align-items: center;
justify-content: center;
.chart-icon {
font-size: 2rem;
}
}
}
}

View File

@@ -1,22 +0,0 @@
.timelineGraph {
display: flex;
flex-direction: column;
gap: var(--spacing-10);
background: var(--l2-background);
padding: var(--spacing-6);
border-radius: 4px;
border: 1px solid var(--l1-border);
height: 150px;
}
.timelineGraphTitle {
width: max-content;
padding: var(--spacing-1) var(--spacing-4);
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
font-size: var(--periscope-font-size-small);
line-height: var(--line-height-18);
letter-spacing: -0.06px;
}

View File

@@ -4,7 +4,7 @@ import DataStateRenderer from 'periscope/components/DataStateRenderer/DataStateR
import Graph from '../Graph/Graph';
import styles from './GraphWrapper.module.scss';
import '../Graph/Graph.styles.scss';
function GraphWrapper({
totalCurrentTriggers,
@@ -40,11 +40,11 @@ function GraphWrapper({
// }, [startTime]);
return (
<div className={styles.timelineGraph}>
<div className={styles.timelineGraphTitle}>
<div className="timeline-graph">
<div className="timeline-graph__title">
{totalCurrentTriggers} triggers in {relativeTime}
</div>
<div>
<div className="timeline-graph__chart">
<DataStateRenderer
isLoading={isLoading}
isError={isError || !isValidRuleId || !ruleId}

View File

@@ -1,70 +0,0 @@
.timelineTable {
border-top: 1px solid var(--l1-border);
border-radius: 6px;
overflow: hidden;
margin-top: var(--spacing-2);
min-height: 600px;
:global(.ant-table) {
background: var(--l1-background);
}
:global(.ant-table-cell) {
padding: var(--spacing-6) var(--spacing-8) !important;
vertical-align: baseline;
&::before {
display: none;
}
}
:global(.ant-table-thead) > tr > th {
border-color: var(--l1-border);
background: var(--l1-background);
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-medium);
padding: var(--spacing-6) var(--spacing-8) var(--spacing-4) !important;
}
:global(.ant-table-tbody) > tr > td {
border: none;
}
:global(.ant-table.ant-table-middle) {
border-bottom: 1px solid var(--l1-border);
border-left: 1px solid var(--l1-border);
border-right: 1px solid var(--l1-border);
border-radius: 6px;
}
:global(.ant-pagination-item-active) {
display: flex;
width: var(--spacing-10);
height: var(--spacing-10);
align-items: center;
justify-content: center;
padding: var(--spacing-0) var(--spacing-4);
border-radius: 2px;
background: var(--primary-background);
& > a {
color: var(--primary-foreground);
line-height: var(--line-height-20);
font-weight: var(--font-weight-medium);
}
}
}
.alertRuleCreatedAt {
font-size: var(--periscope-font-size-base);
color: var(--l2-foreground);
line-height: var(--line-height-18);
letter-spacing: -0.07px;
}
.alertHistoryLabelSearch {
:global(.ant-select-selector) {
border: none;
background: var(--l2-background);
}
}

View File

@@ -0,0 +1,89 @@
.timeline-table {
border-top: 1px solid var(--l1-border);
border-radius: 6px;
overflow: hidden;
margin-top: 4px;
min-height: 600px;
.ant-table {
background: var(--l1-background);
&-cell {
padding: 12px 16px !important;
vertical-align: baseline;
&::before {
display: none;
}
}
&-thead > tr > th {
border-color: var(--l1-border);
background: var(--l1-background);
font-size: 12px;
font-weight: 500;
padding: 12px 16px 8px !important;
}
&-tbody > tr > td {
border: none;
}
}
.label-filter {
padding: 6px 8px;
border-radius: 4px;
background: var(--l1-foreground);
border-width: 0;
line-height: 18px;
& ::placeholder {
color: var(--l2-foreground);
font-size: 12px;
letter-spacing: 0.6px;
text-transform: uppercase;
font-weight: 500;
}
}
.alert-rule {
&-value,
&__created-at {
font-size: 14px;
color: var(--l2-foreground);
}
&-value {
font-weight: 500;
line-height: 20px;
}
&__created-at {
line-height: 18px;
letter-spacing: -0.07px;
}
}
.ant-table.ant-table-middle {
border-bottom: 1px solid var(--l1-border);
border-left: 1px solid var(--l1-border);
border-right: 1px solid var(--l1-border);
border-radius: 6px;
}
.ant-pagination-item {
&-active {
display: flex;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
padding: 1px 8px;
border-radius: 2px;
background: var(--primary-background);
& > a {
color: var(--primary-foreground);
line-height: 20px;
font-weight: 500;
}
}
}
.alert-history-label-search {
.ant-select-selector {
border: none;
background: var(--l2-background);
}
}
}

View File

@@ -13,7 +13,7 @@ import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { timelineTableColumns } from './useTimelineTable';
import styles from './Table.module.scss';
import './Table.styles.scss';
function TimelineTable(): JSX.Element {
const [filters, setFilters] = useState<TagFilter>(initialFilters);
@@ -54,7 +54,7 @@ function TimelineTable(): JSX.Element {
});
return (
<div className={styles.timelineTable}>
<div className="timeline-table">
<Table
rowKey={(row): string => `${row.fingerprint}-${row.value}-${row.unixMilli}`}
columns={timelineTableColumns({

View File

@@ -17,8 +17,6 @@ import AlertState from 'pages/AlertDetails/AlertHeader/AlertState/AlertState';
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import styles from './Table.module.scss';
const transformLabelsToQbKeys = (
labels: AlertRuleTimelineTableResponse['labels'],
): AttributeKey[] => Object.keys(labels).flatMap((key) => [{ key }]);
@@ -58,7 +56,7 @@ function LabelFilter({
<ClientSideQBSearch
onChange={handleSearch}
filters={filters}
className={styles.alertHistoryLabelSearch}
className="alert-history-label-search"
attributeKeys={transformedKeys}
attributeValuesMap={attributesMap}
suffixIcon={
@@ -90,21 +88,29 @@ export const timelineTableColumns = ({
dataIndex: 'state',
sorter: true,
width: 140,
render: (value): JSX.Element => <AlertState state={value} showLabel />,
render: (value): JSX.Element => (
<div className="alert-rule-state">
<AlertState state={value} showLabel />
</div>
),
},
{
title: (
<LabelFilter setFilters={setFilters} filters={filters} labels={labels} />
),
dataIndex: 'labels',
render: (labels): JSX.Element => <AlertLabels labels={labels} />,
render: (labels): JSX.Element => (
<div className="alert-rule-labels">
<AlertLabels labels={labels} />
</div>
),
},
{
title: 'CREATED AT',
dataIndex: 'unixMilli',
width: 200,
render: (value): JSX.Element => (
<div className={styles.alertRuleCreatedAt}>
<div className="alert-rule__created-at">
{formatTimezoneAdjustedTimestamp(value, DATE_TIME_FORMATS.DASH_DATETIME)}
</div>
),
@@ -119,7 +125,7 @@ export const timelineTableColumns = ({
relatedLogsLink={record.relatedLogsLink}
>
<Button type="text" ghost>
<Ellipsis size="md" />
<Ellipsis className="dropdown-icon" size="md" />
</Button>
</ConditionalAlertPopover>
),

View File

@@ -1,35 +0,0 @@
.timelineTabsAndFilters {
display: flex;
justify-content: space-between;
align-items: center;
}
.resetButton,
.top5Contributors {
display: flex;
align-items: center;
gap: var(--spacing-5);
}
.comingSoon {
display: inline-flex;
padding: var(--spacing-2) var(--spacing-4);
border-radius: 20px;
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
justify-content: center;
align-items: center;
gap: var(--spacing-2);
}
.comingSoonText {
color: var(--text-sienna-400);
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-medium);
letter-spacing: -0.05px;
line-height: normal;
}
.comingSoonIcon {
display: flex;
}

View File

@@ -0,0 +1,32 @@
.timeline-tabs-and-filters {
display: flex;
justify-content: space-between;
align-items: center;
.reset-button,
.top-5-contributors {
display: flex;
align-items: center;
gap: 10px;
}
.coming-soon {
display: inline-flex;
padding: 4px 8px;
border-radius: 20px;
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
justify-content: center;
align-items: center;
gap: 5px;
&__text {
color: var(--text-sienna-400);
font-size: 10px;
font-weight: 500;
letter-spacing: -0.05px;
line-height: normal;
}
&__icon {
display: flex;
}
}
}

View File

@@ -6,13 +6,13 @@ import history from 'lib/history';
import { Info } from '@signozhq/icons';
import Tabs2 from 'periscope/components/Tabs2';
import styles from './TabsAndFilters.module.scss';
import './TabsAndFilters.styles.scss';
function ComingSoon(): JSX.Element {
return (
<div className={styles.comingSoon}>
<div className={styles.comingSoonText}>Coming Soon</div>
<div className={styles.comingSoonIcon}>
<div className="coming-soon">
<div className="coming-soon__text">Coming Soon</div>
<div className="coming-soon__icon">
<Info size={10} color={Color.BG_SIENNA_400} />
</div>
</div>
@@ -27,7 +27,7 @@ function TimelineTabs(): JSX.Element {
{
value: TimelineTab.TOP_5_CONTRIBUTORS,
label: (
<div className={styles.top5Contributors}>
<div className="top-5-contributors">
Top 5 Contributors
<ComingSoon />
</div>
@@ -80,7 +80,7 @@ function TimelineFilters(): JSX.Element {
function TabsAndFilters(): JSX.Element {
return (
<div className={styles.timelineTabsAndFilters}>
<div className="timeline-tabs-and-filters">
<TimelineTabs />
<TimelineFilters />
</div>

View File

@@ -1,14 +0,0 @@
.timeline {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
margin: 0 var(--spacing-8);
}
.timelineTitle {
color: var(--l1-foreground);
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}

View File

@@ -0,0 +1,14 @@
.timeline {
display: flex;
flex-direction: column;
gap: 8px;
margin: 0 16px;
&__title {
color: var(--l1-foreground);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
}

View File

@@ -2,7 +2,7 @@ import GraphWrapper from './GraphWrapper/GraphWrapper';
import TimelineTable from './Table/Table';
import TabsAndFilters from './TabsAndFilters/TabsAndFilters';
import styles from './Timeline.module.scss';
import './Timeline.styles.scss';
function TimelineTableRenderer(): JSX.Element {
return <TimelineTable />;
@@ -14,15 +14,15 @@ function Timeline({
totalCurrentTriggers: number;
}): JSX.Element {
return (
<div className={styles.timeline}>
<div className={styles.timelineTitle}>Timeline</div>
<div>
<div className="timeline">
<div className="timeline__title">Timeline</div>
<div className="timeline__tabs-and-filters">
<TabsAndFilters />
</div>
<div>
<div className="timeline__graph">
<GraphWrapper totalCurrentTriggers={totalCurrentTriggers} />
</div>
<div>
<div className="timeline__table">
<TimelineTableRenderer />
</div>
</div>

View File

@@ -1,183 +0,0 @@
.anomalyAlertEvaluationView {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: var(--spacing-4);
width: 100%;
height: 100%;
:global(.uplot-tooltip) {
background-color: rgb(0 0 0 / 90%);
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
color: #ddd;
font-size: var(--periscope-font-size-base);
line-height: 1.4;
padding: var(--spacing-4) var(--spacing-6);
pointer-events: none;
position: absolute;
z-index: 100;
max-height: 500px;
width: 280px;
overflow-y: auto;
display: none;
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136 136 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
:global(.uplot-tooltip-title) {
font-weight: var(--font-weight-semibold);
margin-bottom: var(--spacing-2);
}
:global(.uplot-tooltip-series) {
display: flex;
gap: var(--spacing-2);
padding: var(--spacing-2) 0;
align-items: center;
}
:global(.uplot-tooltip-series-name) {
margin-right: var(--spacing-2);
}
:global(.uplot-tooltip-band) {
font-style: italic;
color: #666;
}
:global(.uplot-tooltip-marker) {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: var(--spacing-4);
vertical-align: middle;
}
:global(.uplot) {
:global(.u-title) {
text-align: center;
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-normal);
display: flex;
height: 40px;
align-items: center;
}
:global(.u-legend) {
display: flex;
margin-top: var(--spacing-8);
tbody {
width: 100%;
:global(.u-series) {
display: inline-flex;
}
}
}
}
}
.chartSection {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.chartSectionMultiSeries {
composes: chartSection;
width: calc(100% - 240px);
}
.noDataContainer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: var(--spacing-4);
}
.seriesSelection {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
width: 240px;
padding: 0 var(--spacing-4);
height: 100%;
}
.seriesList {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
height: 100%;
}
.seriesListSearch {
margin-bottom: var(--spacing-8);
}
.seriesListTitle {
margin-top: var(--spacing-6);
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-normal);
}
.seriesListItems {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
height: 100%;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.1rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136 136 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
.seriesListItem {
display: flex;
flex-direction: row;
gap: var(--spacing-4);
cursor: pointer;
}
.seriesListItemColor {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-flex;
margin-right: var(--spacing-4);
vertical-align: middle;
}

View File

@@ -0,0 +1,180 @@
.anomaly-alert-evaluation-view {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 8px;
width: 100%;
height: 100%;
.anomaly-alert-evaluation-view-chart-section {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
&.has-multi-series-data {
width: calc(100% - 240px);
}
.anomaly-alert-evaluation-view-no-data-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
}
}
.anomaly-alert-evaluation-view-series-selection {
display: flex;
flex-direction: column;
gap: 8px;
width: 240px;
padding: 0px 8px;
height: 100%;
.anomaly-alert-evaluation-view-series-list {
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
.anomaly-alert-evaluation-view-series-list-search {
margin-bottom: 16px;
}
.anomaly-alert-evaluation-view-series-list-title {
margin-top: 12px;
font-size: 13px !important;
font-weight: 400;
}
.anomaly-alert-evaluation-view-series-list-items {
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
overflow-y: auto;
.anomaly-alert-evaluation-view-series-list-item {
display: flex;
flex-direction: row;
gap: 8px;
.anomaly-alert-evaluation-view-series-list-item-color {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-flex;
margin-right: 8px;
vertical-align: middle;
}
cursor: pointer;
}
&::-webkit-scrollbar {
width: 0.1rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
}
}
.uplot {
.u-title {
text-align: center;
font-size: 18px;
font-weight: 400;
display: flex;
height: 40px;
font-size: 13px;
align-items: center;
}
.u-legend {
display: flex;
margin-top: 16px;
tbody {
width: 100%;
.u-series {
display: inline-flex;
}
}
}
}
}
.uplot-tooltip {
background-color: rgba(0, 0, 0, 0.9);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
color: #ddd;
font-size: 13px;
line-height: 1.4;
padding: 8px 12px;
pointer-events: none;
position: absolute;
z-index: 100;
max-height: 500px;
width: 280px;
overflow-y: auto;
display: none; /* Hide tooltip by default */
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
.uplot-tooltip-title {
font-weight: bold;
margin-bottom: 4px;
}
.uplot-tooltip-series {
display: flex;
gap: 4px;
padding: 4px 0px;
align-items: center;
}
.uplot-tooltip-series-name {
margin-right: 4px;
}
.uplot-tooltip-band {
font-style: italic;
color: #666;
}
.uplot-tooltip-marker {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
}

View File

@@ -15,7 +15,7 @@ import uPlot from 'uplot';
import tooltipPlugin from './tooltipPlugin';
import 'uplot/dist/uPlot.min.css';
import styles from './AnomalyAlertEvaluationView.module.scss';
import './AnomalyAlertEvaluationView.styles.scss';
const { Search } = Input;
@@ -284,11 +284,11 @@ function AnomalyAlertEvaluationView({
}, 300);
return (
<div className={styles.anomalyAlertEvaluationView}>
<div className="anomaly-alert-evaluation-view">
<div
className={
allSeries.length > 1 ? styles.chartSectionMultiSeries : styles.chartSection
}
className={`anomaly-alert-evaluation-view-chart-section ${
allSeries.length > 1 ? 'has-multi-series-data' : ''
}`}
ref={graphRef}
>
{allSeries.length > 0 ? (
@@ -298,7 +298,7 @@ function AnomalyAlertEvaluationView({
chartRef={chartRef}
/>
) : (
<div className={styles.noDataContainer}>
<div className="anomaly-alert-evaluation-view-no-data-container">
<ChartLine size={48} strokeWidth={0.5} />
<Typography>No Data</Typography>
@@ -307,20 +307,20 @@ function AnomalyAlertEvaluationView({
</div>
{allSeries.length > 1 && (
<div className={styles.seriesSelection}>
<div className="anomaly-alert-evaluation-view-series-selection">
{allSeries.length > 1 && (
<div className={styles.seriesList}>
<div className="anomaly-alert-evaluation-view-series-list">
<Search
className={styles.seriesListSearch}
className="anomaly-alert-evaluation-view-series-list-search"
placeholder="Search a series"
allowClear
onChange={handleSearchValueChange}
/>
<div className={styles.seriesListItems}>
<div className="anomaly-alert-evaluation-view-series-list-items">
{filteredSeriesKeys.length > 0 && (
<Checkbox
className={styles.seriesListItem}
className="anomaly-alert-evaluation-view-series-list-item"
name="series"
value={selectedSeries === null}
onChange={(): void => handleSeriesChange(null)}
@@ -332,14 +332,14 @@ function AnomalyAlertEvaluationView({
{filteredSeriesKeys.map((seriesKey) => (
<div key={seriesKey}>
<Checkbox
className={styles.seriesListItem}
className="anomaly-alert-evaluation-view-series-list-item"
key={seriesKey}
name="series"
value={selectedSeries === seriesKey}
onChange={(): void => handleSeriesChange(seriesKey)}
>
<div
className={styles.seriesListItemColor}
className="anomaly-alert-evaluation-view-series-list-item-color"
style={{ backgroundColor: seriesData[seriesKey].color }}
/>

View File

@@ -1,69 +0,0 @@
.createAlertTabsExtra {
display: flex;
align-items: center;
gap: var(--spacing-8);
}
.createAlertWrapper {
margin-top: var(--spacing-5);
:global(.divider) {
border-color: var(--l1-border);
margin: var(--spacing-8) 0;
}
:global(.breadcrumb-divider) {
margin-top: var(--spacing-5);
}
}
.createAlertBreadcrumb {
padding-left: var(--spacing-8);
:global(.breadcrumb-item) {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
line-height: var(--line-height-20);
letter-spacing: 0.25px;
padding: 0;
}
:global(.ant-breadcrumb-separator),
:global(.breadcrumb-item--last) {
color: var(--muted-foreground);
font-family: var(--periscope-font-family-mono);
}
}
.createAlertBreadcrumb ol {
align-items: center;
}
.alertsContainer {
:global(.top-level-tab.periscope-tab) {
padding: var(--spacing-1) 0;
}
:global(.ant-tabs-nav) {
padding: 0 var(--spacing-4);
margin-bottom: 0 !important;
&::before {
border-bottom: 1px solid var(--l1-border) !important;
}
}
:global(.ant-tabs-tab) {
&[data-node-key='TriggeredAlerts'] {
margin-left: var(--spacing-8);
}
&:not(:first-of-type) {
margin-left: var(--spacing-10) !important;
}
}
:global(.ant-tabs-tab) [aria-selected='false'] :global(.periscope-tab) {
color: var(--l2-foreground);
}
}

View File

@@ -0,0 +1,75 @@
.create-alert-tabs {
&__extra {
display: flex;
align-items: center;
gap: 16px;
}
}
.create-alert-wrapper {
margin-top: 10px;
.divider {
border-color: var(--l1-border);
margin: 16px 0;
}
.breadcrumb-divider {
margin-top: 10px;
}
}
.create-alert__breadcrumb {
padding-left: 16px;
ol {
align-items: center;
}
.breadcrumb-item {
color: var(--l2-foreground);
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
padding: 0;
}
.ant-breadcrumb-separator,
.breadcrumb-item--last {
color: var(--muted-foreground);
font-family: 'Geist Mono';
}
}
.alerts-container {
.top-level-tab.periscope-tab {
padding: 2px 0;
}
.ant-tabs {
&-nav {
padding: 0 8px;
margin-bottom: 0 !important;
&::before {
border-bottom: 1px solid var(--l1-border) !important;
}
}
&-tab {
&[data-node-key='TriggeredAlerts'] {
margin-left: 16px;
}
&:not(:first-of-type) {
margin-left: 24px !important;
}
[aria-selected='false'] {
.periscope-tab {
color: var(--l2-foreground);
}
}
}
}
}

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo } from 'react';
import cx from 'classnames';
import { Form, Tabs, TabsProps } from 'antd';
import logEvent from 'api/common/logEvent';
import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon';
@@ -23,7 +22,7 @@ import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
import { ALERTS_VALUES_MAP, ALERT_TYPE_BREADCRUMB_TITLE } from './defaults';
import SelectAlertType from './SelectAlertType';
import styles from './CreateAlertRule.module.scss';
import './CreateAlertRule.styles.scss';
function CreateRules(): JSX.Element {
const [formInstance] = Form.useForm();
@@ -144,9 +143,9 @@ function CreateRules(): JSX.Element {
),
key: AlertListTabs.ALERT_RULES,
children: (
<div className={styles.createAlertWrapper}>
<div className="create-alert-wrapper">
<AlertBreadcrumb
className={styles.createAlertBreadcrumb}
className="create-alert__breadcrumb"
items={
isTypeSelectionMode
? [
@@ -191,9 +190,9 @@ function CreateRules(): JSX.Element {
items={items}
activeKey={AlertListTabs.ALERT_RULES}
onChange={handleTabChange}
className={cx(styles.alertsContainer, 'create-alert-tabs')}
className="alerts-container create-alert-tabs"
tabBarExtraContent={
<div className={styles.createAlertTabsExtra}>
<div className="create-alert-tabs__extra">
<DateTimeSelector showAutoRefresh />
<HeaderRightSection
enableAnnouncements={false}

View File

@@ -1,66 +0,0 @@
.alertConditionContainer {
margin: 0 var(--spacing-8);
margin-top: var(--spacing-12);
}
.alertCondition {
display: flex;
align-items: center;
margin-left: var(--spacing-6);
margin-top: var(--spacing-12);
}
.alertConditionTabs {
display: flex;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
flex-direction: row;
border-bottom: none;
margin-bottom: -1px;
}
.explorerViewOption {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
border: none;
padding: 9px;
box-shadow: none;
border-radius: 0;
border-left: 0.5px solid var(--l1-border);
border-bottom: 0.5px solid var(--l1-border);
width: 120px;
height: 36px;
gap: var(--spacing-4);
&:first-child {
border-left: 1px solid transparent;
}
&:hover {
background-color: transparent !important;
border-left: 1px solid transparent !important;
color: var(--l1-foreground);
}
&:disabled {
background-color: var(--l2-background);
opacity: 0.6;
}
}
.activeTab {
background-color: var(--l1-background);
border-bottom: none;
&:hover {
background-color: var(--l1-background) !important;
}
}
.condensedAdvancedOptionsContainer {
margin-top: var(--spacing-8);
width: fit-parent;
}

View File

@@ -15,7 +15,7 @@ import AlertThreshold from './AlertThreshold';
import AnomalyThreshold from './AnomalyThreshold';
import { ANOMALY_TAB_TOOLTIP, THRESHOLD_TAB_TOOLTIP } from './constants';
import styles from './AlertCondition.module.scss';
import './styles.scss';
function AlertCondition(): JSX.Element {
const { alertType, setAlertType } = useCreateAlertState();
@@ -67,15 +67,15 @@ function AlertCondition(): JSX.Element {
};
return (
<div className={styles.alertConditionContainer}>
<div className="alert-condition-container">
<Stepper stepNumber={2} label="Set alert conditions" />
<div className={styles.alertCondition}>
<div className={styles.alertConditionTabs}>
<div className="alert-condition">
<div className="alert-condition-tabs">
{tabs.map((tab) => (
<Tooltip key={tab.value} title={getTabTooltip(tab)}>
<Button
className={classNames(styles.explorerViewOption, {
[styles.activeTab]: alertType === tab.value,
className={classNames('list-view-tab', 'explorer-view-option', {
'active-tab': alertType === tab.value,
})}
onClick={(): void => {
if (alertType !== tab.value) {
@@ -106,7 +106,7 @@ function AlertCondition(): JSX.Element {
refreshChannels={refreshChannels}
/>
)}
<div className={styles.condensedAdvancedOptionsContainer}>
<div className="condensed-advanced-options-container">
<AdvancedOptions />
</div>
</div>

View File

@@ -1,84 +0,0 @@
.alertThresholdContainer {
padding: var(--spacing-12);
padding-right: 72px;
background-color: var(--l1-background);
border: 1px solid var(--l1-border);
width: 100%;
}
.alertConditionSentences {
display: flex;
flex-direction: column;
gap: var(--spacing-6);
}
.alertConditionSentence {
display: flex;
align-items: center;
gap: var(--spacing-8);
flex-wrap: wrap;
:global(.ant-select) {
width: 240px;
:global(.ant-select-selector) {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--muted-foreground);
font-family: 'Space Mono';
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
}
:global(.ant-select-selection-item) {
color: var(--l1-foreground);
}
:global(.ant-select-arrow) {
color: var(--muted-foreground);
}
}
}
.sentenceText {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
line-height: 1.5;
--typography-text-display: flex;
align-items: center;
gap: var(--spacing-4);
}
.thresholdsSection {
margin-top: var(--spacing-8);
margin-left: var(--spacing-12);
}
.addThresholdBtn {
margin-top: var(--spacing-4);
border: 1px dashed var(--l1-border);
color: var(--l2-foreground);
background-color: transparent;
border-radius: 4px;
height: 32px;
padding: 0 var(--spacing-8);
display: flex;
align-items: center;
justify-content: center;
&:hover {
border-color: var(--l1-border);
color: var(--l1-foreground);
}
:global(.anticon) {
margin-right: var(--spacing-4);
}
}

View File

@@ -1,6 +1,7 @@
import { useEffect } from 'react';
import { Button, Select, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import classNames from 'classnames';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import getRandomColor from 'lib/getRandomColor';
import { Plus } from '@signozhq/icons';
@@ -31,7 +32,8 @@ import {
RoutingPolicyBanner,
} from './utils';
import styles from './AlertThreshold.module.scss';
import './styles.scss';
import '../EvaluationSettings/styles.scss';
function AlertThreshold({
channels,
@@ -217,11 +219,16 @@ function AlertThreshold({
};
return (
<div className={styles.alertThresholdContainer}>
<div
className={classNames(
'alert-threshold-container',
'condensed-alert-threshold-container',
)}
>
{/* Main condition sentence */}
<div className={styles.alertConditionSentences}>
<div className={styles.alertConditionSentence}>
<Typography.Text className={styles.sentenceText}>
<div className="alert-condition-sentences">
<div className="alert-condition-sentence">
<Typography.Text className="sentence-text">
Send a notification when
</Typography.Text>
<Select
@@ -231,7 +238,7 @@ function AlertThreshold({
options={queryNames}
data-testid="alert-threshold-query-select"
/>
<Typography.Text className={styles.sentenceText}>is</Typography.Text>
<Typography.Text className="sentence-text">is</Typography.Text>
<Select
value={
(normalizeOperator(thresholdState.operator) ??
@@ -247,7 +254,7 @@ function AlertThreshold({
options={THRESHOLD_OPERATOR_OPTIONS}
data-testid="alert-threshold-operator-select"
/>
<Typography.Text className={styles.sentenceText}>
<Typography.Text className="sentence-text">
the threshold(s)
</Typography.Text>
<Select
@@ -265,13 +272,13 @@ function AlertThreshold({
options={matchTypeOptionsWithTooltips}
data-testid="alert-threshold-match-type-select"
/>
<Typography.Text className={styles.sentenceText}>
<Typography.Text className="sentence-text">
during the <EvaluationSettings />
</Typography.Text>
</div>
</div>
<div className={styles.thresholdsSection}>
<div className="thresholds-section">
{thresholdState.thresholds.map((threshold, index) => (
<ThresholdItem
key={threshold.id}
@@ -290,7 +297,7 @@ function AlertThreshold({
type="dashed"
icon={<Plus size={16} />}
onClick={addThreshold}
className={styles.addThresholdBtn}
className="add-threshold-btn"
data-testid="add-threshold-button"
>
Add Threshold

View File

@@ -1,63 +0,0 @@
.anomalyThresholdContainer {
padding: var(--spacing-12);
padding-right: 72px;
background-color: var(--l1-background);
border: 1px solid var(--l1-border);
width: 100%;
:global(.ant-select) {
:global(.ant-select-selector) {
min-width: 150px;
}
}
}
.alertConditionSentences {
display: flex;
flex-direction: column;
gap: var(--spacing-6);
}
.alertConditionSentence {
display: flex;
align-items: center;
gap: var(--spacing-8);
flex-wrap: wrap;
:global(.ant-select) {
width: 240px;
:global(.ant-select-selector) {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--muted-foreground);
font-family: 'Space Mono';
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
}
:global(.ant-select-selection-item) {
color: var(--l1-foreground);
}
:global(.ant-select-arrow) {
color: var(--muted-foreground);
}
}
}
.sentenceText {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
line-height: 1.5;
--typography-text-display: flex;
align-items: center;
gap: var(--spacing-4);
}

View File

@@ -24,8 +24,6 @@ import {
RoutingPolicyBanner,
} from './utils';
import styles from './AnomalyThreshold.module.scss';
function AnomalyThreshold({
channels,
isLoadingChannels,
@@ -66,14 +64,11 @@ function AnomalyThreshold({
};
return (
<div className={styles.anomalyThresholdContainer}>
<div className={styles.alertConditionSentences}>
<div className="anomaly-threshold-container">
<div className="alert-condition-sentences">
{/* Sentence 1 */}
<div className={styles.alertConditionSentence}>
<Typography.Text
data-testid="notification-text"
className={styles.sentenceText}
>
<div className="alert-condition-sentence">
<Typography.Text data-testid="notification-text" className="sentence-text">
Send notification when the observed value for
</Typography.Text>
<Select
@@ -89,7 +84,7 @@ function AnomalyThreshold({
/>
<Typography.Text
data-testid="evaluation-window-text"
className={styles.sentenceText}
className="sentence-text"
>
during the last
</Typography.Text>
@@ -105,12 +100,9 @@ function AnomalyThreshold({
options={ANOMALY_TIME_DURATION_OPTIONS}
/>
</div>
<div className={styles.alertConditionSentence}>
<div className="alert-condition-sentence">
{/* Sentence 2 */}
<Typography.Text
data-testid="threshold-text"
className={styles.sentenceText}
>
<Typography.Text data-testid="threshold-text" className="sentence-text">
is
</Typography.Text>
<Select
@@ -125,10 +117,7 @@ function AnomalyThreshold({
}}
options={deviationOptions}
/>
<Typography.Text
data-testid="deviations-text"
className={styles.sentenceText}
>
<Typography.Text data-testid="deviations-text" className="sentence-text">
deviations
</Typography.Text>
<Select
@@ -147,7 +136,7 @@ function AnomalyThreshold({
/>
<Typography.Text
data-testid="predicted-data-text"
className={styles.sentenceText}
className="sentence-text"
>
the predicted data
</Typography.Text>
@@ -167,11 +156,8 @@ function AnomalyThreshold({
/>
</div>
{/* Sentence 3 */}
<div className={styles.alertConditionSentence}>
<Typography.Text
data-testid="using-the-text"
className={styles.sentenceText}
>
<div className="alert-condition-sentence">
<Typography.Text data-testid="using-the-text" className="sentence-text">
using the
</Typography.Text>
<Select
@@ -187,7 +173,7 @@ function AnomalyThreshold({
/>
<Typography.Text
data-testid="algorithm-with-text"
className={styles.sentenceText}
className="sentence-text"
>
algorithm with
</Typography.Text>
@@ -206,7 +192,7 @@ function AnomalyThreshold({
<>
<Typography.Text
data-testid="seasonality-text"
className={styles.sentenceText}
className="sentence-text"
>
seasonality to
</Typography.Text>
@@ -242,10 +228,7 @@ function AnomalyThreshold({
/>
</>
) : (
<Typography.Text
data-testid="seasonality-text"
className={styles.sentenceText}
>
<Typography.Text data-testid="seasonality-text" className="sentence-text">
seasonality
</Typography.Text>
)}

View File

@@ -1,105 +0,0 @@
.thresholdItem {
display: flex;
flex-direction: column;
gap: 0;
margin-bottom: var(--spacing-8);
}
.thresholdRow {
display: flex;
align-items: center;
gap: var(--spacing-8);
margin-bottom: 2px;
}
.thresholdIndicator {
display: flex;
}
.thresholdDot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
.thresholdControls {
display: flex;
align-items: center;
gap: var(--spacing-4);
flex-wrap: wrap;
:global(.ant-input) {
background-color: var(--card);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
height: 32px;
&::placeholder {
font-family: 'Space Mono';
}
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
}
:global(.ant-select) {
:global(.ant-select-selector) {
background-color: var(--card);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
height: 32px;
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
:global(.ant-select-selection-placeholder) {
font-family: 'Space Mono';
}
}
:global(.ant-select-selection-item) {
color: var(--l1-foreground);
}
:global(.ant-select-arrow) {
color: var(--muted-foreground);
}
}
}
.iconBtn {
color: var(--muted-foreground);
border: 1px solid var(--l1-border);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.sentenceText {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
line-height: 1.5;
--typography-text-display: flex;
align-items: center;
gap: var(--spacing-4);
}
.highlightedText {
font-weight: bold;
color: var(--bg-robin-400);
margin: 0 4px;
}

View File

@@ -11,8 +11,6 @@ import { normalizeOperator } from '../utils';
import { ThresholdItemProps } from './types';
import { NotificationChannelsNotFoundContent } from './utils';
import styles from './ThresholdItem.module.scss';
function ThresholdItem({
threshold,
updateThreshold,
@@ -84,16 +82,15 @@ function ThresholdItem({
};
return (
<div key={threshold.id} className={styles.thresholdItem}>
<div className={styles.thresholdRow}>
<div className={styles.thresholdIndicator}>
<div key={threshold.id} className="threshold-item">
<div className="threshold-row">
<div className="threshold-indicator">
<div
className={styles.thresholdDot}
className="threshold-dot"
style={{ backgroundColor: threshold.color }}
data-testid="threshold-dot"
/>
</div>
<div className={styles.thresholdControls}>
<div className="threshold-controls">
<Input
placeholder="Enter threshold name"
value={threshold.label}
@@ -103,10 +100,8 @@ function ThresholdItem({
style={{ width: 200 }}
data-testid="threshold-name-input"
/>
<Typography.Text className={styles.sentenceText}>on value</Typography.Text>
<Typography.Text
className={`${styles.sentenceText} ${styles.highlightedText}`}
>
<Typography.Text className="sentence-text">on value</Typography.Text>
<Typography.Text className="sentence-text highlighted-text">
{getOperatorSymbol()}
</Typography.Text>
<Input
@@ -122,9 +117,7 @@ function ThresholdItem({
{yAxisUnitSelect}
{!notificationSettings.routingPolicies && (
<>
<Typography.Text className={styles.sentenceText}>
send to
</Typography.Text>
<Typography.Text className="sentence-text">send to</Typography.Text>
<Select
value={threshold.channels}
onChange={(value): void =>
@@ -161,9 +154,7 @@ function ThresholdItem({
)}
{showRecoveryThreshold && (
<>
<Typography.Text className={styles.sentenceText}>
recover on
</Typography.Text>
<Typography.Text className="sentence-text">recover on</Typography.Text>
<Input
placeholder="Enter recovery threshold value"
value={threshold.recoveryThresholdValue ?? ''}
@@ -179,7 +170,7 @@ function ThresholdItem({
type="default"
icon={<Trash size={16} />}
onClick={removeRecoveryThreshold}
className={styles.iconBtn}
className="icon-btn"
data-testid="remove-recovery-threshold-button"
/>
</Tooltip>
@@ -203,7 +194,7 @@ function ThresholdItem({
type="default"
icon={<CircleX size={16} />}
onClick={(): void => removeThreshold(threshold.id)}
className={styles.iconBtn}
className="icon-btn"
data-testid="remove-threshold-button"
/>
</Tooltip>

View File

@@ -25,6 +25,7 @@ const THRESHOLD_VIEW_TEST_ID = 'threshold-view';
const ANOMALY_VIEW_TEST_ID = 'anomaly-view';
const ANOMALY_TAB_TEXT = 'Anomaly';
const THRESHOLD_TAB_TEXT = 'Threshold';
const ACTIVE_TAB_CLASS = '.active-tab';
// Mock the Stepper component
jest.mock('../../Stepper', () => ({
@@ -129,9 +130,9 @@ describe('AlertCondition', () => {
// screen.queryByTestId(ANOMALY_THRESHOLD_TEST_ID),
// ).not.toBeInTheDocument();
// Verify threshold tab exists
// Verify threshold tab is active by default
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
expect(thresholdTab).toBeInTheDocument();
expect(thresholdTab.closest(ACTIVE_TAB_CLASS)).toBeInTheDocument();
// Verify both tabs are visible (METRICS_BASED_ALERT supports multiple tabs)
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
@@ -205,24 +206,22 @@ describe('AlertCondition', () => {
});
// TODO: Unskip this when anomaly tab is implemented
// Note: Active tab styling is verified through component behavior (correct content shown)
// rather than CSS class checks since CSS modules classes are mocked in tests
it.skip('applies active tab styling correctly', () => {
renderAlertCondition();
// Threshold tab should be active by default - verify by checking content
expect(screen.getByTestId(ALERT_THRESHOLD_TEST_ID)).toBeInTheDocument();
expect(
screen.queryByTestId(ANOMALY_THRESHOLD_TEST_ID),
).not.toBeInTheDocument();
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
const anomalyTab = screen.getByText(ANOMALY_TAB_TEXT);
// Threshold tab should be active by default
expect(thresholdTab.closest(ACTIVE_TAB_CLASS)).toBeInTheDocument();
expect(anomalyTab.closest(ACTIVE_TAB_CLASS)).not.toBeInTheDocument();
// Click anomaly tab
const anomalyTab = screen.getByText(ANOMALY_TAB_TEXT);
fireEvent.click(anomalyTab);
// Anomaly tab should be active now - verify by checking content
expect(screen.getByTestId(ANOMALY_THRESHOLD_TEST_ID)).toBeInTheDocument();
expect(screen.queryByTestId(ALERT_THRESHOLD_TEST_ID)).not.toBeInTheDocument();
// Anomaly tab should be active now
expect(anomalyTab.closest(ACTIVE_TAB_CLASS)).toBeInTheDocument();
expect(thresholdTab.closest(ACTIVE_TAB_CLASS)).not.toBeInTheDocument();
});
it('shows multiple tabs for METRICS_BASED_ALERT', () => {

View File

@@ -126,8 +126,8 @@ describe('ThresholdItem', () => {
it('renders threshold indicator with correct color', () => {
renderThresholdItem();
// Find the threshold dot by data-testid
const thresholdDot = screen.getByTestId('threshold-dot');
// Find the threshold dot by its class
const thresholdDot = document.querySelector('.threshold-dot');
expect(thresholdDot).toHaveStyle('background-color: #ff0000');
});

View File

@@ -0,0 +1,406 @@
.alert-condition-container {
margin: 0 16px;
margin-top: 24px;
.alert-condition {
display: flex;
align-items: center;
margin-left: 12px;
margin-top: 24px;
.alert-condition-tabs {
display: flex;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
flex-direction: row;
border-bottom: none;
margin-bottom: -1px;
.explorer-view-option {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
border: none;
padding: 9px;
box-shadow: none;
border-radius: 0px;
border-left: 0.5px solid var(--l1-border);
border-bottom: 0.5px solid var(--l1-border);
width: 120px;
height: 36px;
gap: 8px;
&.active-tab {
background-color: var(--l1-background);
border-bottom: none;
&:hover {
background-color: var(--l1-background) !important;
}
}
&:disabled {
background-color: var(--l2-background);
opacity: 0.6;
}
&:first-child {
border-left: 1px solid transparent;
}
&:hover {
background-color: transparent !important;
border-left: 1px solid transparent !important;
color: var(--l1-foreground);
}
}
}
}
}
.alert-threshold-container,
.anomaly-threshold-container {
padding: 24px;
padding-right: 72px;
background-color: var(--l1-background);
border: 1px solid var(--l1-border);
width: 100%;
.alert-condition-sentences {
display: flex;
flex-direction: column;
gap: 12px;
.alert-condition-sentence {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
.sentence-text {
color: var(--l2-foreground);
font-size: 13px;
line-height: 1.5;
display: flex;
align-items: center;
gap: 8px;
}
.ant-select {
width: 240px;
.ant-select-selector {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--muted-foreground);
font-family: 'Space Mono';
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
}
.ant-select-selection-item {
color: var(--l1-foreground);
}
.ant-select-arrow {
color: var(--muted-foreground);
}
}
}
}
.thresholds-section {
margin-top: 16px;
margin-left: 24px;
.threshold-item {
display: flex;
flex-direction: column;
gap: 0;
margin-bottom: 16px;
.threshold-row {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 2px;
.threshold-indicator {
.threshold-dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
}
.threshold-controls {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
.ant-input {
background-color: var(--card);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
height: 32px;
&::placeholder {
font-family: 'Space Mono';
}
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
}
.ant-select {
.ant-select-selector {
background-color: var(--card);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
height: 32px;
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
.ant-select-selection-placeholder {
font-family: 'Space Mono';
}
}
.ant-select-selection-item {
color: var(--l1-foreground);
}
.ant-select-arrow {
color: var(--muted-foreground);
}
}
.icon-btn {
color: var(--muted-foreground);
border: 1px solid var(--l1-border);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.recovery-threshold-input-group {
display: flex;
align-items: center;
gap: 0;
margin-left: 28px;
.recovery-threshold-label {
pointer-events: none;
cursor: default;
}
.recovery-threshold-btn {
pointer-events: none;
cursor: default;
color: var(--muted-foreground);
background-color: var(--card) !important;
border: 1px solid var(--l1-border);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.ant-input {
background-color: var(--card);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
height: 32px;
&::placeholder {
font-family: 'Space Mono';
}
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
}
}
}
.add-threshold-btn {
margin-top: 8px;
border: 1px dashed var(--l1-border);
color: var(--l2-foreground);
background-color: transparent;
border-radius: 4px;
height: 32px;
padding: 0 16px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
border-color: var(--l1-border);
color: var(--l1-foreground);
}
.anticon {
margin-right: 8px;
}
}
}
.routing-policies-info-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 16px;
background-color: color-mix(
in srgb,
var(--primary-background) 10%,
transparent
);
border: 1px solid var(--primary-background);
padding: 8px 16px;
.routing-policies-info-banner-right {
display: flex;
align-items: center;
gap: 8px;
.view-routing-policies-button {
color: var(--accent-primary);
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
}
}
.ant-typography {
color: var(--accent-primary);
}
}
}
.anomaly-threshold-container {
.ant-select {
.ant-select-selector {
min-width: 150px;
}
}
}
.condensed-alert-threshold-container,
.condensed-anomaly-threshold-container {
width: 100%;
}
.condensed-advanced-options-container {
margin-top: 16px;
width: fit-parent;
}
.condensed-evaluation-settings-container {
.ant-btn {
display: flex;
align-items: center;
min-width: 240px;
width: auto;
justify-content: space-between;
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
.evaluate-alert-conditions-button-left {
color: var(--l2-foreground);
font-size: 12px;
flex-shrink: 0;
}
.evaluate-alert-conditions-button-right {
display: flex;
align-items: center;
color: var(--l2-foreground);
gap: 8px;
flex-shrink: 0;
.evaluate-alert-conditions-button-right-text {
font-size: 12px;
font-weight: 500;
background-color: var(--l1-border);
padding: 1px 4px;
}
}
}
}
.highlighted-text {
font-weight: bold;
color: var(--bg-robin-400);
margin: 0 4px;
}
// Tooltip styles
.tooltip-content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
.tooltip-description {
margin-bottom: 8px;
span {
font-weight: bold;
color: var(--bg-robin-400);
}
}
.tooltip-example {
margin-bottom: 8px;
color: var(--l2-foreground);
}
.tooltip-link {
.tooltip-link-text {
color: var(--accent-primary);
font-size: 11px;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}

View File

@@ -1,68 +0,0 @@
.routingPoliciesInfoBanner {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-4);
margin-top: var(--spacing-8);
background-color: color-mix(
in srgb,
var(--primary-background) 10%,
transparent
);
border: 1px solid var(--primary-background);
padding: var(--spacing-4) var(--spacing-8);
:global(.ant-typography) {
color: var(--accent-primary);
}
}
.routingPoliciesInfoBannerRight {
display: flex;
align-items: center;
gap: var(--spacing-4);
}
.viewRoutingPoliciesButton {
color: var(--accent-primary);
font-size: var(--periscope-font-size-small);
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
}
.tooltipContent {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
.tooltipExample {
margin-bottom: var(--spacing-4);
color: var(--l2-foreground);
}
.tooltipLink {
display: block;
}
.tooltipLinkText {
color: var(--accent-primary);
font-size: var(--periscope-font-size-small);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.tooltipDescription {
margin-bottom: var(--spacing-4);
span {
font-weight: bold;
color: var(--bg-robin-400);
}
}

View File

@@ -22,8 +22,6 @@ import { openInNewTab } from 'utils/navigation';
import { ROUTING_POLICIES_ROUTE } from './constants';
import { RoutingPolicyBannerProps } from './types';
import styles from './utils.module.scss';
export function getQueryNames(currentQuery: Query): BaseOptionType[] {
const involvedQueriesInTraceOperator = getInvolvedQueriesInTraceOperator(
currentQuery.builder.queryTraceOperator,
@@ -185,7 +183,7 @@ function TooltipContent({
handleTooltipClick(e);
}
}}
className={styles.tooltipContent}
className="tooltip-content"
>
{children}
</div>
@@ -206,7 +204,7 @@ function TooltipExample({
matchType: AlertThresholdMatchType;
}): JSX.Element {
return (
<div className={styles.tooltipExample}>
<div className="tooltip-example">
<strong>Example:</strong>
<br />
Say, For a 5-minute window (configured in Evaluation settings), 1 min
@@ -222,12 +220,12 @@ function TooltipExample({
function TooltipLink(): JSX.Element {
return (
<div className={styles.tooltipLink}>
<div className="tooltip-link">
<a
href="https://signoz.io/docs"
target="_blank"
rel="noopener noreferrer"
className={styles.tooltipLinkText}
className="tooltip-link-text"
>
Learn more
</a>
@@ -263,7 +261,7 @@ export const getMatchTypeTooltip = (
case AlertThresholdMatchType.AT_LEAST_ONCE:
return (
<TooltipContent>
<div className={styles.tooltipDescription}>
<div className="tooltip-description">
Data is aggregated at each interval within your evaluation window,
creating multiple data points. This option triggers if <span>ANY</span> of
those aggregated data points crosses the threshold.
@@ -284,7 +282,7 @@ export const getMatchTypeTooltip = (
case AlertThresholdMatchType.ALL_THE_TIME:
return (
<TooltipContent>
<div className={styles.tooltipDescription}>
<div className="tooltip-description">
Data is aggregated at each interval within your evaluation window,
creating multiple data points. This option triggers if <span>ALL</span>{' '}
aggregated data points cross the threshold.
@@ -308,7 +306,7 @@ export const getMatchTypeTooltip = (
).toFixed(1);
return (
<TooltipContent>
<div className={styles.tooltipDescription}>
<div className="tooltip-description">
Data is aggregated at each interval within your evaluation window,
creating multiple data points. This option triggers if the{' '}
<span>AVERAGE</span> of all aggregated data points crosses the threshold.
@@ -330,7 +328,7 @@ export const getMatchTypeTooltip = (
const total = dataPoints.reduce((a, b) => a + b, 0);
return (
<TooltipContent>
<div className={styles.tooltipDescription}>
<div className="tooltip-description">
Data is aggregated at each interval within your evaluation window,
creating multiple data points. This option triggers if the{' '}
<span>SUM</span> of all aggregated data points crosses the threshold.
@@ -352,7 +350,7 @@ export const getMatchTypeTooltip = (
const lastPoint = dataPoints[dataPoints.length - 1];
return (
<TooltipContent>
<div className={styles.tooltipDescription}>
<div className="tooltip-description">
Data is aggregated at each interval within your evaluation window,
creating multiple data points. This option triggers based on the{' '}
<span>MOST RECENT</span> aggregated data point only.
@@ -416,11 +414,11 @@ export function RoutingPolicyBanner({
}: RoutingPolicyBannerProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
return (
<div className={styles.routingPoliciesInfoBanner}>
<div className="routing-policies-info-banner">
<Typography.Text>
Use <strong>Routing Policies</strong> for dynamic routing
</Typography.Text>
<div className={styles.routingPoliciesInfoBannerRight}>
<div className="routing-policies-info-banner-right">
<Switch
value={notificationSettings.routingPolicies}
testId="routing-policies-switch"
@@ -433,7 +431,7 @@ export function RoutingPolicyBanner({
/>
<Button
type="link"
className={styles.viewRoutingPoliciesButton}
className="view-routing-policies-button"
data-testid="view-routing-policies-button"
onClick={(): void => safeNavigate(ROUTING_POLICIES_ROUTE)}
>

View File

@@ -1,139 +0,0 @@
.alertHeader {
background-color: var(--l1-background);
font-family: inherit;
color: var(--l1-foreground);
padding: var(--spacing-6) var(--spacing-8);
}
.editAlertHeader {
flex: 1;
}
.tabBar {
display: flex;
align-items: center;
justify-content: space-between;
}
.tab {
display: flex;
align-items: center;
background-color: var(--l1-background);
height: 32px;
font-size: var(--periscope-font-size-base);
color: var(--l1-foreground);
&::before {
content: '';
margin-right: var(--spacing-3);
font-size: var(--periscope-font-size-base);
color: var(--l3-foreground);
}
}
.content {
padding: var(--spacing-4) 0;
background: var(--l1-background);
display: flex;
flex-direction: column;
gap: var(--spacing-4);
min-width: 300px;
flex: 1;
}
.inputTitle {
background-color: transparent;
color: var(--l1-foreground);
width: 100%;
min-width: 300px;
}
.inputDescription {
font-size: var(--periscope-font-size-base);
background-color: transparent;
color: var(--l2-foreground);
}
.labelsInput {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
.labelsInputAddButton {
width: fit-content;
font-size: var(--periscope-font-size-base);
color: var(--l2-foreground);
border: 1px solid var(--l1-border);
background-color: transparent;
cursor: pointer;
padding: var(--spacing-2) var(--spacing-4);
border-radius: 4px;
&:hover {
border-color: var(--l1-border);
color: var(--l1-foreground);
}
}
.labelsInputExistingLabels {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-4);
}
.labelsInputLabelPill {
display: inline-flex;
align-items: center;
gap: var(--spacing-3);
background-color: #ad7f581a;
color: var(--bg-sienna-400);
padding: var(--spacing-2) var(--spacing-4);
border-radius: 16px;
font-size: var(--periscope-font-size-small);
border: 1px solid var(--bg-sienna-500);
font-family: 'Geist Mono';
}
.labelsInputRemoveButton {
background: none;
border: none;
color: var(--bg-sienna-400);
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
&:hover {
color: var(--l1-foreground);
}
}
.labelsInputInputContainer {
display: flex;
align-items: center;
background-color: transparent;
border: none;
}
.labelsInputInput {
flex: 1;
background-color: transparent;
border: none;
outline: none;
padding: var(--spacing-3) var(--spacing-4);
color: var(--l1-foreground);
font-size: var(--periscope-font-size-base);
&::placeholder {
color: var(--l2-foreground);
}
&:focus,
&:active {
border: none;
outline: none;
}
}

View File

@@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import classNames from 'classnames';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -14,7 +14,8 @@ import { Labels } from 'types/api/alerts/def';
import { useCreateAlertState } from '../context';
import LabelsInput from './LabelsInput';
import styles from './CreateAlertHeader.module.scss';
import './styles.scss';
function CreateAlertHeader(): JSX.Element {
const { alertState, setAlertState, isEditMode } = useCreateAlertState();
@@ -55,11 +56,11 @@ function CreateAlertHeader(): JSX.Element {
return (
<div
className={cx(styles.alertHeader, { [styles.editAlertHeader]: isEditMode })}
className={classNames('alert-header', { 'edit-alert-header': isEditMode })}
>
{!isEditMode && (
<div className={styles.tabBar}>
<div className={styles.tab}>New Alert Rule</div>
<div className="alert-header__tab-bar">
<div className="alert-header__tab">New Alert Rule</div>
<Button
prefix={<RotateCcw size={12} />}
onClick={handleSwitchToClassicExperience}
@@ -71,7 +72,7 @@ function CreateAlertHeader(): JSX.Element {
</Button>
</div>
)}
<div className={styles.content}>
<div className="alert-header__content">
<Input
type="text"
value={alertState.name}
@@ -82,7 +83,7 @@ function CreateAlertHeader(): JSX.Element {
alertRuleContext.setAlertRuleName(newName);
}
}}
className={styles.inputTitle}
className="alert-header__input title"
placeholder="Enter alert rule name"
data-testid="alert-name-input"
/>

View File

@@ -3,7 +3,6 @@ import { X } from '@signozhq/icons';
import { useNotifications } from 'hooks/useNotifications';
import { LabelInputState, LabelsInputProps } from './types';
import styles from './CreateAlertHeader.module.scss';
function LabelsInput({
labels,
@@ -121,19 +120,19 @@ function LabelsInput({
}, [inputState]);
return (
<div className={styles.labelsInput}>
<div className="labels-input">
{Object.keys(labels).length > 0 && (
<div className={styles.labelsInputExistingLabels}>
<div className="labels-input__existing-labels">
{Object.entries(labels).map(([key, value]) => (
<span
key={key}
className={styles.labelsInputLabelPill}
className="labels-input__label-pill"
data-testid={`label-pill-${key}-${value}`}
>
{key}: {value}
<button
type="button"
className={styles.labelsInputRemoveButton}
className="labels-input__remove-button"
aria-label={`Remove label ${key}`}
onClick={(): void => handleRemoveLabel(key)}
>
@@ -146,7 +145,7 @@ function LabelsInput({
{!isAdding ? (
<button
className={styles.labelsInputAddButton}
className="labels-input__add-button"
type="button"
onClick={handleAddLabelsClick}
data-testid="alert-add-label-button"
@@ -154,7 +153,7 @@ function LabelsInput({
+ Add labels
</button>
) : (
<div className={styles.labelsInputInputContainer}>
<div className="labels-input__input-container">
<input
autoFocus
type="text"
@@ -162,7 +161,7 @@ function LabelsInput({
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className={styles.labelsInputInput}
className="labels-input__input"
placeholder={inputState.isKeyInput ? 'Enter key' : 'Enter value'}
data-testid="alert-add-label-input"
/>

View File

@@ -0,0 +1,145 @@
.alert-header {
background-color: var(--l1-background);
font-family: inherit;
color: var(--l1-foreground);
padding: 12px 16px;
&__tab-bar {
display: flex;
align-items: center;
justify-content: space-between;
}
/* Tab block visuals */
&__tab {
display: flex;
align-items: center;
background-color: var(--l1-background);
height: 32px;
font-size: 13px;
color: var(--l1-foreground);
}
&__tab::before {
content: '';
margin-right: 6px;
font-size: 13px;
color: var(--l3-foreground);
}
&__content {
padding: 8px 0;
background: var(--l1-background);
display: flex;
flex-direction: column;
gap: 8px;
min-width: 300px;
flex: 1;
}
&__input.title {
background-color: transparent;
color: var(--l1-foreground);
width: 100%;
min-width: 300px;
}
&__input.description {
font-size: 13px;
background-color: transparent;
color: var(--l2-foreground);
}
.ant-btn {
display: flex;
gap: 4px;
align-items: center;
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
margin-right: 16px;
}
}
.labels-input {
display: flex;
flex-direction: column;
gap: 8px;
&__add-button {
width: fit-content;
font-size: 13px;
color: var(--l2-foreground);
border: 1px solid var(--l1-border);
background-color: transparent;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
&:hover {
border-color: var(--l1-border);
color: var(--l1-foreground);
}
}
&__existing-labels {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
&__label-pill {
display: inline-flex;
align-items: center;
gap: 6px;
background-color: #ad7f581a;
color: var(--bg-sienna-400);
padding: 4px 8px;
border-radius: 16px;
font-size: 12px;
border: 1px solid var(--bg-sienna-500);
font-family: 'Geist Mono';
}
&__remove-button {
background: none;
border: none;
color: var(--bg-sienna-400);
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
&:hover {
color: var(--l1-foreground);
}
}
&__input-container {
display: flex;
align-items: center;
background-color: transparent;
border: none;
}
&__input {
flex: 1;
background-color: transparent;
border: none;
outline: none;
padding: 6px 8px;
color: var(--l1-foreground);
font-size: 13px;
&::placeholder {
color: var(--l2-foreground);
}
&:focus,
&:active {
border: none;
outline: none;
}
}
}

View File

@@ -1,14 +1,14 @@
.createAlertV2Container {
.create-alert-v2-container {
background-color: var(--l1-background);
padding-bottom: 100px;
}
.stickyPageSpinner {
.sticky-page-spinner {
position: fixed;
inset: 0;
display: grid;
place-items: center;
background: rgb(0 0 0 / 35%);
background: rgba(0, 0, 0, 0.35);
z-index: 10000;
pointer-events: auto;
}

View File

@@ -12,7 +12,7 @@ import QuerySection from './QuerySection';
import { CreateAlertV2Props } from './types';
import { Spinner } from './utils';
import styles from './CreateAlertV2.module.scss';
import './CreateAlertV2.styles.scss';
function CreateAlertV2({ alertType }: CreateAlertV2Props): JSX.Element {
const queryToRedirect = buildInitialAlertDef(alertType);
@@ -25,7 +25,7 @@ function CreateAlertV2({ alertType }: CreateAlertV2Props): JSX.Element {
return (
<CreateAlertProvider initialAlertType={alertType}>
<Spinner />
<div className={styles.createAlertV2Container}>
<div className="create-alert-v2-container">
<CreateAlertHeader />
<QuerySection />
<AlertCondition />

View File

@@ -5,7 +5,8 @@ import { Typography } from '@signozhq/ui/typography';
import { Info } from '@signozhq/icons';
import { IAdvancedOptionItemProps } from '../types';
import styles from './styles.module.scss';
import './styles.scss';
function AdvancedOptionItem({
title,
@@ -28,9 +29,9 @@ function AdvancedOptionItem({
};
return (
<div className={styles.advancedOptionItem} data-testid={dataTestId}>
<div className={styles.advancedOptionItemLeftContent}>
<Typography.Text className={styles.advancedOptionItemTitle}>
<div className="advanced-option-item" data-testid={dataTestId}>
<div className="advanced-option-item-left-content">
<Typography.Text className="advanced-option-item-title">
{title}
{tooltipText && (
<Tooltip title={tooltipText}>
@@ -38,13 +39,13 @@ function AdvancedOptionItem({
</Tooltip>
)}
</Typography.Text>
<Typography.Text className={styles.advancedOptionItemDescription}>
<Typography.Text className="advanced-option-item-description">
{description}
</Typography.Text>
</div>
<div className={styles.advancedOptionItemRightContent}>
<div className="advanced-option-item-right-content">
<div
className={styles.advancedOptionItemInput}
className="advanced-option-item-input"
style={{ display: showInput ? 'block' : 'none' }}
>
{input}

View File

@@ -1,150 +0,0 @@
.advancedOptionItem {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: var(--spacing-8);
border-bottom: 1px solid var(--l1-border);
}
.advancedOptionItemLeftContent {
display: flex;
flex-direction: column;
gap: var(--spacing-3);
}
.advancedOptionItemTitle {
color: var(--l2-foreground);
font-family: Inter;
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-medium);
--typography-text-display: flex;
align-items: center;
gap: var(--spacing-4);
}
.advancedOptionItemDescription {
color: var(--muted-foreground);
font-family: Inter;
font-size: var(--periscope-font-size-base);
font-weight: 400;
}
.advancedOptionItemInput {
margin-top: var(--spacing-8);
:global(.ant-input) {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
height: 32px;
&::placeholder {
font-family: 'Space Mono';
}
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
}
:global(.ant-select) {
:global(.ant-select-selector) {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
height: 32px;
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
:global(.ant-select-selection-placeholder) {
font-family: 'Space Mono';
}
}
:global(.ant-select-selection-item) {
color: var(--l1-foreground);
}
:global(.ant-select-arrow) {
color: var(--l2-foreground);
}
}
}
.advancedOptionItemRightContent {
display: flex;
align-items: flex-start;
gap: var(--spacing-8);
}
.advancedOptionItemInputGroup {
display: flex;
align-items: center;
gap: var(--spacing-4);
:global(.ant-input) {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
}
:global(.ant-select) {
:global(.ant-select-selector) {
background-color: var(--l2-background);
color: var(--l1-foreground);
height: 32px;
border: 1px solid var(--l1-border);
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
}
:global(.ant-select-selection-placeholder) {
font-family: 'Space Mono';
}
}
:global(.ant-select-selection-item) {
color: var(--l1-foreground);
}
:global(.ant-select-arrow) {
color: var(--l2-foreground);
}
}
}
.advancedOptionItemButton {
display: flex;
align-items: center;
gap: var(--spacing-4);
background-color: var(--l3-background);
border: 1px solid var(--l1-border);
color: var(--l2-foreground);
border-radius: 4px;
}

View File

@@ -0,0 +1,150 @@
.advanced-option-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16px;
border-bottom: 1px solid var(--l1-border);
.advanced-option-item-left-content {
display: flex;
flex-direction: column;
gap: 6px;
.advanced-option-item-title {
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.advanced-option-item-description {
color: var(--muted-foreground);
font-family: Inter;
font-size: 13px;
font-weight: 400;
}
.advanced-option-item-input {
margin-top: 16px;
.ant-input {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
height: 32px;
&::placeholder {
font-family: 'Space Mono';
}
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
}
.ant-select {
.ant-select-selector {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
height: 32px;
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
.ant-select-selection-placeholder {
font-family: 'Space Mono';
}
}
.ant-select-selection-item {
color: var(--l1-foreground);
}
.ant-select-arrow {
color: var(--l2-foreground);
}
}
}
}
.advanced-option-item-right-content {
display: flex;
align-items: flex-start;
gap: 16px;
.advanced-option-item-input-group {
display: flex;
align-items: center;
gap: 8px;
.ant-input {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
}
.ant-select {
.ant-select-selector {
background-color: var(--l2-background);
color: var(--l1-foreground);
height: 32px;
border: 1px solid var(--l1-border);
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
}
.ant-select-selection-placeholder {
font-family: 'Space Mono';
}
}
.ant-select-selection-item {
color: var(--l1-foreground);
}
.ant-select-arrow {
color: var(--l2-foreground);
}
}
}
.advanced-option-item-button {
display: flex;
align-items: center;
gap: 8px;
background-color: var(--l3-background);
border: 1px solid var(--l1-border);
color: var(--l2-foreground);
border-radius: 4px;
}
}
}

View File

@@ -4,15 +4,13 @@ import { Typography } from '@signozhq/ui/typography';
import { useCreateAlertState } from '../context';
import AdvancedOptionItem from './AdvancedOptionItem';
import advancedOptionStyles from './AdvancedOptionItem/styles.module.scss';
import EvaluationCadence from './EvaluationCadence';
import styles from './styles.module.scss';
function AdvancedOptions(): JSX.Element {
const { advancedOptions, setAdvancedOptions } = useCreateAlertState();
return (
<div className={styles.advancedOptionsContainer}>
<div className="advanced-options-container">
<Collapse bordered={false}>
<Collapse.Panel header="ADVANCED OPTIONS" key="1">
<EvaluationCadence />
@@ -21,7 +19,7 @@ function AdvancedOptions(): JSX.Element {
description="Send notification if no data is received for a specified time period."
tooltipText="Useful for monitoring data pipelines or services that should continuously send data. For example, alert if no logs are received for 10 minutes"
input={
<div className={advancedOptionStyles.advancedOptionItemInputGroup}>
<div className="advanced-option-item-input-group">
<Input
placeholder="Enter tolerance limit..."
type="number"
@@ -54,7 +52,7 @@ function AdvancedOptions(): JSX.Element {
description="Only trigger alert when there are enough data points to make a reliable decision."
tooltipText="Prevents false alarms when there's insufficient data. For example, require at least 5 data points before checking if CPU usage is above 80%."
input={
<div className={advancedOptionStyles.advancedOptionItemInputGroup}>
<div className="advanced-option-item-input-group">
<Input
placeholder="Enter minimum datapoints..."
style={{ width: 100 }}

View File

@@ -6,8 +6,6 @@ import { INITIAL_ADVANCED_OPTIONS_STATE } from 'container/CreateAlertV2/context/
import { IEditCustomScheduleProps } from 'container/CreateAlertV2/EvaluationSettings/types';
import { Calendar1, Pencil, Trash } from '@signozhq/icons';
import styles from './styles.module.scss';
function EditCustomSchedule({
setIsEvaluationCadenceDetailsVisible,
setIsPreviewVisible,
@@ -19,7 +17,7 @@ function EditCustomSchedule({
return (
<Typography.Text>
<Typography.Text>Every</Typography.Text>
<Typography.Text className={styles.highlight}>
<Typography.Text className="highlight">
{advancedOptions.evaluationCadence.custom.repeatEvery
.charAt(0)
.toUpperCase() +
@@ -28,7 +26,7 @@ function EditCustomSchedule({
{advancedOptions.evaluationCadence.custom.repeatEvery !== 'day' && (
<>
<Typography.Text>on</Typography.Text>
<Typography.Text className={styles.highlight}>
<Typography.Text className="highlight">
{advancedOptions.evaluationCadence.custom.occurence
.map(
(occurence) => occurence.charAt(0).toUpperCase() + occurence.slice(1),
@@ -38,7 +36,7 @@ function EditCustomSchedule({
</>
)}
<Typography.Text>at</Typography.Text>
<Typography.Text className={styles.highlight}>
<Typography.Text className="highlight">
{advancedOptions.evaluationCadence.custom.startAt}
</Typography.Text>
</Typography.Text>
@@ -47,11 +45,11 @@ function EditCustomSchedule({
return (
<Typography.Text>
<Typography.Text>Starting on</Typography.Text>
<Typography.Text className={styles.highlight}>
<Typography.Text className="highlight">
{advancedOptions.evaluationCadence.rrule.date?.format('DD/MM/YYYY')}
</Typography.Text>
<Typography.Text>at</Typography.Text>
<Typography.Text className={styles.highlight}>
<Typography.Text className="highlight">
{advancedOptions.evaluationCadence.rrule.startAt}
</Typography.Text>
</Typography.Text>
@@ -79,9 +77,9 @@ function EditCustomSchedule({
};
return (
<div className={styles.editCustomSchedule} data-testid="edit-custom-schedule">
<div className="edit-custom-schedule">
{displayText}
<div>
<div className="button-row">
<Button.Group>
<Button type="default" onClick={handleEdit}>
<Pencil size={12} />

View File

@@ -8,8 +8,9 @@ import { ADVANCED_OPTIONS_TIME_UNIT_OPTIONS } from '../../context/constants';
import EditCustomSchedule from './EditCustomSchedule';
import EvaluationCadenceDetails from './EvaluationCadenceDetails';
import EvaluationCadencePreview from './EvaluationCadencePreview';
import advancedOptionStyles from '../AdvancedOptionItem/styles.module.scss';
import styles from './styles.module.scss';
import './styles.scss';
import '../AdvancedOptionItem/styles.scss';
function EvaluationCadence(): JSX.Element {
const { advancedOptions, setAdvancedOptions } = useCreateAlertState();
@@ -40,31 +41,25 @@ function EvaluationCadence(): JSX.Element {
// };
return (
<div className={styles.evaluationCadenceContainer}>
<div
className={`${advancedOptionStyles.advancedOptionItem} ${styles.evaluationCadenceItem}`}
>
<div className={advancedOptionStyles.advancedOptionItemLeftContent}>
<Typography.Text className={advancedOptionStyles.advancedOptionItemTitle}>
<div className="evaluation-cadence-container">
<div className="advanced-option-item evaluation-cadence-item">
<div className="advanced-option-item-left-content">
<Typography.Text className="advanced-option-item-title">
How often to check
<Tooltip title="Controls how frequently the alert evaluates your conditions. For most alerts, 1-5 minutes is sufficient.">
<Info data-testid="evaluation-cadence-tooltip-icon" size={16} />
</Tooltip>
</Typography.Text>
<Typography.Text
className={advancedOptionStyles.advancedOptionItemDescription}
>
<Typography.Text className="advanced-option-item-description">
How frequently this alert checks your data. Default: Every 1 minute
</Typography.Text>
</div>
{isCustomScheduleButtonVisible && (
<div
className={advancedOptionStyles.advancedOptionItemRightContent}
className="advanced-option-item-right-content"
data-testid="evaluation-cadence-input-group"
>
<Input.Group
className={advancedOptionStyles.advancedOptionItemInputGroup}
>
<Input.Group className="advanced-option-item-input-group">
<Input
type="number"
placeholder="Enter time"

View File

@@ -21,7 +21,6 @@ import {
isValidRRule,
} from '../utils';
import { ScheduleList } from './EvaluationCadencePreview';
import styles from './styles.module.scss';
function EvaluationCadenceDetails({
setIsOpen,
@@ -91,8 +90,8 @@ function EvaluationCadenceDetails({
}, [evaluationCadence.custom.repeatEvery]);
const EditorView = (
<div className={styles.editorView} data-testid="editor-view">
<div className={styles.selectGroup}>
<div className="editor-view" data-testid="editor-view">
<div className="select-group">
<Typography.Text>REPEAT EVERY</Typography.Text>
<Select
options={EVALUATION_CADENCE_REPEAT_EVERY_OPTIONS}
@@ -114,7 +113,7 @@ function EvaluationCadenceDetails({
/>
</div>
{evaluationCadence.custom.repeatEvery !== 'day' && (
<div className={styles.selectGroup}>
<div className="select-group">
<Typography.Text>ON DAY(S)</Typography.Text>
<Select
options={occurenceOptions}
@@ -136,7 +135,7 @@ function EvaluationCadenceDetails({
/>
</div>
)}
<div className={styles.selectGroup}>
<div className="select-group">
<Typography.Text>AT</Typography.Text>
<TimeInput
value={evaluationCadence.custom.startAt}
@@ -151,7 +150,7 @@ function EvaluationCadenceDetails({
}
/>
</div>
<div className={styles.selectGroup}>
<div className="select-group">
<Typography.Text>TIMEZONE</Typography.Text>
<Select
options={TIMEZONE_DATA}
@@ -175,8 +174,8 @@ function EvaluationCadenceDetails({
);
const RRuleView = (
<div className={styles.rruleView} data-testid="rrule-view">
<div className={styles.selectGroup}>
<div className="rrule-view" data-testid="rrule-view">
<div className="select-group">
<Typography.Text>STARTING ON</Typography.Text>
<DatePicker
value={evaluationCadence.rrule.date}
@@ -192,7 +191,7 @@ function EvaluationCadenceDetails({
placeholder="Select date"
/>
</div>
<div className={styles.selectGroup}>
<div className="select-group">
<Typography.Text>AT</Typography.Text>
<TimeInput
value={evaluationCadence.rrule.startAt}
@@ -295,19 +294,19 @@ function EvaluationCadenceDetails({
};
return (
<div className={styles.evaluationCadenceDetails}>
<Typography.Text className={styles.evaluationCadenceDetailsTitle}>
<div className="evaluation-cadence-details">
<Typography.Text className="evaluation-cadence-details-title">
Add Custom Schedule
</Typography.Text>
<div className={styles.evaluationCadenceDetailsContent}>
<div className={styles.evaluationCadenceDetailsContentRow}>
<div className={styles.querySectionTabs}>
<div className={styles.querySectionQueryActions}>
<div className="evaluation-cadence-details-content">
<div className="evaluation-cadence-details-content-row">
<div className="query-section-tabs">
<div className="query-section-query-actions">
{tabs.map((tab) => (
<Button
key={tab.value}
className={classNames(styles.explorerViewOption, {
[styles.activeTab]: activeTab === tab.value,
className={classNames('list-view-tab', 'explorer-view-option', {
'active-tab': activeTab === tab.value,
})}
onClick={(): void => {
handleChangeTab(tab.value as 'editor' | 'rrule');
@@ -321,7 +320,7 @@ function EvaluationCadenceDetails({
</div>
{activeTab === 'editor' && EditorView}
{activeTab === 'rrule' && RRuleView}
<div className={styles.buttonsRow}>
<div className="buttons-row">
<Button type="default" onClick={handleDiscard}>
Discard
</Button>
@@ -334,7 +333,7 @@ function EvaluationCadenceDetails({
</Button>
</div>
</div>
<div className={styles.evaluationCadenceDetailsContentRow}>
<div className="evaluation-cadence-details-content-row">
<ScheduleList
schedule={schedule}
currentTimezone={evaluationCadence.custom.timezone}

View File

@@ -10,7 +10,6 @@ import {
buildAlertScheduleFromCustomSchedule,
buildAlertScheduleFromRRule,
} from '../utils';
import styles from './styles.module.scss';
export function ScheduleList({
schedule,
@@ -18,21 +17,21 @@ export function ScheduleList({
}: IScheduleListProps): JSX.Element {
if (schedule && schedule.length > 0) {
return (
<div className={styles.schedulePreview} data-testid="schedule-preview">
<div className={styles.schedulePreviewHeader}>
<div className="schedule-preview" data-testid="schedule-preview">
<div className="schedule-preview-header">
<Calendar size={16} />
<Typography.Text className={styles.schedulePreviewTitle}>
<Typography.Text className="schedule-preview-title">
Schedule Preview
</Typography.Text>
</div>
<div className={styles.schedulePreviewList}>
<div className="schedule-preview-list">
{schedule.map((date) => (
<div key={date.toISOString()} className={styles.schedulePreviewItem}>
<div className={styles.schedulePreviewTimeline}>
<div className={styles.schedulePreviewTimelineLine} />
<div key={date.toISOString()} className="schedule-preview-item">
<div className="schedule-preview-timeline">
<div className="schedule-preview-timeline-line" />
</div>
<div className={styles.schedulePreviewContent}>
<div className={styles.schedulePreviewDate}>
<div className="schedule-preview-content">
<div className="schedule-preview-date">
{date.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
@@ -46,8 +45,8 @@ export function ScheduleList({
second: '2-digit',
})}
</div>
<div className={styles.schedulePreviewSeparator} />
<div className={styles.schedulePreviewTimezone}>
<div className="schedule-preview-separator" />
<div className="schedule-preview-timezone">
{
TIMEZONE_DATA.find((timezone) => timezone.value === currentTimezone)
?.label
@@ -62,7 +61,7 @@ export function ScheduleList({
}
return (
<div className={styles.noSchedule} data-testid="no-schedule">
<div className="no-schedule" data-testid="no-schedule">
<Info size={32} />
<Typography.Text>
Please fill the relevant information to generate a schedule
@@ -99,19 +98,13 @@ function EvaluationCadencePreview({
open={isOpen}
onCancel={(): void => setIsOpen(false)}
footer={null}
className={styles.evaluationCadencePreviewModal}
className="evaluation-cadence-preview-modal"
width={800}
centered
>
<div
className={`${styles.evaluationCadenceDetails} ${styles.evaluationCadencePreview}`}
>
<div
className={`${styles.evaluationCadenceDetailsContent} ${styles.evaluationCadencePreviewContent}`}
>
<div
className={`${styles.evaluationCadenceDetailsContentRow} ${styles.evaluationCadencePreviewContentRow}`}
>
<div className="evaluation-cadence-details evaluation-cadence-preview">
<div className="evaluation-cadence-details-content">
<div className="evaluation-cadence-details-content-row">
<ScheduleList
schedule={schedule}
currentTimezone={advancedOptions.evaluationCadence.custom.timezone}

View File

@@ -1,3 +1,5 @@
import EvaluationCadence from './EvaluationCadence';
import './styles.scss';
export default EvaluationCadence;

View File

@@ -1,450 +0,0 @@
.evaluationCadenceContainer {
border-bottom: 1px solid var(--l1-border);
}
.evaluationCadenceItem {
border-bottom: none !important;
}
.editCustomSchedule {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-8);
padding: var(--spacing-8);
:global(.ant-typography) {
color: var(--l1-foreground);
font-size: var(--periscope-font-size-base);
}
:global(.ant-btn-group) {
:global(.ant-btn) {
border: 1px solid var(--l1-border);
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
display: flex;
align-items: center;
gap: var(--spacing-4);
}
}
}
.highlight {
background-color: var(--l1-background);
padding: var(--spacing-2) var(--spacing-4);
border-radius: 4px;
color: var(--l2-foreground);
font-weight: var(--font-weight-medium);
margin: 0 var(--spacing-2);
font-size: var(--periscope-font-size-base);
}
.evaluationCadenceDetails {
margin: var(--spacing-8);
display: flex;
flex-direction: column;
gap: var(--spacing-8);
border: 1px solid var(--l1-border);
}
.evaluationCadenceDetailsTitle {
color: var(--l1-foreground);
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-medium);
padding-left: var(--spacing-8);
padding-top: var(--spacing-8);
}
.querySectionTabs {
display: flex;
align-items: center;
}
.querySectionQueryActions {
display: flex;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
flex-direction: row;
border-bottom: none;
margin-bottom: -1px;
}
.explorerViewOption {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
border: none;
padding: 9px;
box-shadow: none;
border-radius: 0;
border-left: 0.5px solid var(--l1-border);
border-bottom: 0.5px solid var(--l1-border);
width: 120px;
height: 36px;
gap: var(--spacing-4);
&:first-child {
border-left: 1px solid transparent;
}
&:hover {
background-color: transparent !important;
border-left: 1px solid transparent !important;
color: var(--l1-foreground);
}
&:disabled {
background-color: var(--l2-background);
opacity: 0.6;
}
}
.activeTab {
background-color: var(--l1-background);
border-bottom: none;
&:hover {
background-color: var(--l1-background) !important;
}
}
.evaluationCadenceDetailsContent {
display: flex;
gap: var(--spacing-8);
border-top: 1px solid var(--l1-border);
padding: var(--spacing-8);
}
.evaluationCadenceDetailsContentRow {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
flex: 1;
height: 500px;
overflow-y: scroll;
padding-right: var(--spacing-8);
}
.editorView {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
}
.rruleView {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
textarea {
height: 200px;
background: var(--l2-background);
border: 1px solid var(--l1-border);
border-radius: 4px;
color: var(--l2-foreground) !important;
font-family: 'Space Mono';
font-size: var(--periscope-font-size-base);
&::placeholder {
font-family: 'Space Mono';
color: var(--muted-foreground) !important;
}
}
}
.selectGroup {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
:global(.ant-typography) {
color: var(--l1-foreground);
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-medium);
}
:global(.ant-select) {
border: 1px solid var(--l1-border);
:global(.ant-select-selector) {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
}
}
}
:global(.ant-picker) {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
:global(.ant-picker-input) {
background-color: var(--l2-background);
color: var(--l1-foreground);
}
}
}
.buttonsRow {
display: flex;
align-items: center;
gap: var(--spacing-8);
margin-top: var(--spacing-8);
}
.noSchedule {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: var(--spacing-4);
height: 100%;
color: var(--l1-foreground);
font-size: var(--periscope-font-size-base);
}
.schedulePreview {
display: flex;
flex-direction: column;
width: 100%;
flex: 1;
min-height: 0;
}
.schedulePreviewHeader {
display: flex;
align-items: center;
gap: var(--spacing-4);
padding: var(--spacing-4) 0;
background-color: var(--card);
position: sticky;
top: 0;
z-index: 1;
border-bottom: 1px solid var(--l1-border);
}
.schedulePreviewTitle {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-medium);
}
.schedulePreviewList {
display: flex;
flex-direction: column;
gap: 0;
flex: 1;
overflow-y: auto;
padding-top: var(--spacing-4);
&::-webkit-scrollbar {
width: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--l1-border);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--l3-background);
}
}
.schedulePreviewItem {
display: flex;
align-items: center;
gap: var(--spacing-6);
padding: var(--spacing-4) 0;
}
.schedulePreviewTimeline {
display: flex;
flex-direction: column;
align-items: center;
min-width: 20px;
}
.schedulePreviewTimelineLine {
width: 1px;
height: 20px;
background-color: var(--l2-background);
}
.schedulePreviewContent {
display: flex;
align-items: center;
gap: var(--spacing-6);
flex: 1;
}
.schedulePreviewDate {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
font-weight: 400;
white-space: nowrap;
}
.schedulePreviewSeparator {
flex: 1;
height: 1px;
border-top: 1px dashed var(--l1-border);
}
.schedulePreviewTimezone {
color: var(--muted-foreground);
font-size: 12px;
font-weight: 400;
white-space: nowrap;
}
/* Global styles for ant-picker date panel */
:global(.ant-picker-date-panel) {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
}
:global(.ant-picker-date-panel-layout) {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
}
:global(.ant-picker-date-panel-header) {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
}
/* Custom modal styles for preview */
.evaluationCadencePreviewModal {
:global(.ant-modal-content) {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
border-radius: var(--spacing-4);
}
:global(.ant-modal-header) {
background-color: var(--l2-background);
border-bottom: 1px solid var(--l1-border);
padding: var(--spacing-8) 20px;
:global(.ant-modal-title) {
color: var(--l1-foreground);
font-size: var(--periscope-font-size-medium);
font-weight: var(--font-weight-semibold);
}
}
:global(.ant-modal-close) {
color: var(--l2-foreground);
top: var(--spacing-8);
right: 20px;
&:hover {
color: var(--l1-foreground);
}
}
:global(.ant-modal-body) {
padding: 0;
background-color: var(--l2-background);
}
}
.evaluationCadencePreview {
border: none;
margin: 0;
}
.evaluationCadencePreviewContent {
border-top: none;
padding: 0;
}
.evaluationCadencePreviewContentRow {
height: auto;
max-height: 60vh;
overflow-y: auto;
padding: var(--spacing-6);
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--l2-background);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: var(--l3-background);
}
}
.previewScheduleHeader {
background-color: var(--card);
border-bottom: 1px solid var(--l1-border);
padding: var(--spacing-6) var(--spacing-8);
margin: calc(-1 * var(--spacing-6)) calc(-1 * var(--spacing-6))
var(--spacing-8) calc(-1 * var(--spacing-6));
}
.previewScheduleTitle {
color: var(--l1-foreground);
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-medium);
}
.previewScheduleItem {
padding: var(--spacing-6) 0;
border-bottom: 1px solid var(--l1-border);
&:last-child {
border-bottom: none;
}
}
.previewScheduleTimelineLine {
width: 2px;
height: 24px;
background-color: var(--primary-background);
border-radius: 1px;
}
.previewScheduleDate {
color: var(--l1-foreground);
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-medium);
}
.previewScheduleTimezone {
background-color: var(--l1-background);
padding: var(--spacing-2) var(--spacing-4);
border-radius: 4px;
font-size: 12px;
}
.previewNoSchedule {
min-height: 300px;
padding: 40px var(--spacing-6);
svg {
color: var(--muted-foreground);
}
}

View File

@@ -0,0 +1,453 @@
.evaluation-cadence-container {
border-bottom: 1px solid var(--l1-border);
.evaluation-cadence-item {
border-bottom: none !important;
}
.edit-custom-schedule {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px;
.ant-typography {
color: var(--l1-foreground);
font-size: 13px;
.highlight {
background-color: var(--l1-background);
padding: 4px 8px;
border-radius: 4px;
color: var(--l2-foreground);
font-weight: 500;
margin: 0 4px;
font-size: 13px;
}
}
.ant-btn-group {
.ant-btn {
border: 1px solid var(--l1-border);
color: var(--l2-foreground);
font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
}
}
}
}
.evaluation-cadence-details {
margin: 16px;
display: flex;
flex-direction: column;
gap: 16px;
border: 1px solid var(--l1-border);
.evaluation-cadence-details-title {
color: var(--l1-foreground);
font-size: 13px;
font-weight: 500;
padding-left: 16px;
padding-top: 16px;
}
.query-section-tabs {
display: flex;
align-items: center;
.query-section-query-actions {
display: flex;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
flex-direction: row;
border-bottom: none;
margin-bottom: -1px;
.explorer-view-option {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
border: none;
padding: 9px;
box-shadow: none;
border-radius: 0px;
border-left: 0.5px solid var(--l1-border);
border-bottom: 0.5px solid var(--l1-border);
width: 120px;
height: 36px;
gap: 8px;
&.active-tab {
background-color: var(--l1-background);
border-bottom: none;
&:hover {
background-color: var(--l1-background) !important;
}
}
&:disabled {
background-color: var(--l2-background);
opacity: 0.6;
}
&:first-child {
border-left: 1px solid transparent;
}
&:hover {
background-color: transparent !important;
border-left: 1px solid transparent !important;
color: var(--l1-foreground);
}
}
}
}
.evaluation-cadence-details-content {
display: flex;
gap: 16px;
border-top: 1px solid var(--l1-border);
padding: 16px;
.evaluation-cadence-details-content-row {
display: flex;
flex-direction: column;
gap: 16px;
flex: 1;
height: 500px;
overflow-y: scroll;
padding-right: 16px;
.editor-view,
.rrule-view {
display: flex;
flex-direction: column;
gap: 16px;
textarea {
height: 200px;
background: var(--l2-background);
border: 1px solid var(--l1-border);
border-radius: 4px;
color: var(--l2-foreground) !important;
font-family: 'Space Mono';
font-size: 13px;
&::placeholder {
font-family: 'Space Mono';
color: var(--muted-foreground) !important;
}
}
.select-group {
display: flex;
flex-direction: column;
gap: 4px;
.ant-typography {
color: var(--l1-foreground);
font-size: 13px;
font-weight: 500;
}
.ant-select {
border: 1px solid var(--l1-border);
.ant-select-selector {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
}
}
}
.ant-picker {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
.ant-picker-input {
background-color: var(--l2-background);
color: var(--l1-foreground);
}
}
}
}
.buttons-row {
display: flex;
align-items: center;
gap: 16px;
margin-top: 16px;
}
.no-schedule {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 8px;
height: 100%;
color: var(--l1-foreground);
font-size: 13px;
}
.schedule-preview {
display: flex;
flex-direction: column;
width: 100%;
flex: 1;
min-height: 0;
.schedule-preview-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
background-color: var(--card);
position: sticky;
top: 0;
z-index: 1;
border-bottom: 1px solid var(--l1-border);
.schedule-preview-title {
color: var(--l2-foreground);
font-size: 13px;
font-weight: 500;
}
}
.schedule-preview-list {
display: flex;
flex-direction: column;
gap: 0;
flex: 1;
overflow-y: auto;
padding-top: 8px;
&::-webkit-scrollbar {
width: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--l1-border);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--l3-background);
}
.schedule-preview-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
.schedule-preview-timeline {
display: flex;
flex-direction: column;
align-items: center;
min-width: 20px;
.schedule-preview-timeline-line {
width: 1px;
height: 20px;
background-color: var(--l2-background);
}
}
.schedule-preview-content {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
.schedule-preview-date {
color: var(--l2-foreground);
font-size: 13px;
font-weight: 400;
white-space: nowrap;
}
.schedule-preview-separator {
flex: 1;
height: 1px;
border-top: 1px dashed var(--l1-border);
}
.schedule-preview-timezone {
color: var(--muted-foreground);
font-size: 12px;
font-weight: 400;
white-space: nowrap;
}
}
}
}
}
}
}
}
.ant-picker-date-panel {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
}
.ant-picker-date-panel-layout {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
}
.ant-picker-date-panel-header {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
}
// Custom modal styles for preview
.evaluation-cadence-preview-modal {
.ant-modal-content {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
border-radius: 8px;
}
.ant-modal-header {
background-color: var(--l2-background);
border-bottom: 1px solid var(--l1-border);
padding: 16px 20px;
.ant-modal-title {
color: var(--l1-foreground);
font-size: 16px;
font-weight: 600;
}
}
.ant-modal-close {
color: var(--l2-foreground);
top: 16px;
right: 20px;
&:hover {
color: var(--l1-foreground);
}
}
.ant-modal-body {
padding: 0;
background-color: var(--l2-background);
}
.evaluation-cadence-details {
border: none;
margin: 0;
.evaluation-cadence-details-content {
border-top: none;
padding: 0;
.evaluation-cadence-details-content-row {
height: auto;
max-height: 60vh;
overflow-y: auto;
padding: 12px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--l2-background);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: var(--l3-background);
}
.schedule-preview {
.schedule-preview-header {
background-color: var(--card);
border-bottom: 1px solid var(--l1-border);
padding: 12px 16px;
margin: -12px -12px 16px -12px;
.schedule-preview-title {
color: var(--l1-foreground);
font-size: 13px;
font-weight: 500;
}
}
.schedule-preview-list {
.schedule-preview-item {
padding: 12px 0;
border-bottom: 1px solid var(--l1-border);
&:last-child {
border-bottom: none;
}
.schedule-preview-timeline {
.schedule-preview-timeline-line {
width: 2px;
height: 24px;
background-color: var(--primary-background);
border-radius: 1px;
}
}
.schedule-preview-content {
.schedule-preview-date {
color: var(--l1-foreground);
font-size: 13px;
font-weight: 500;
}
.schedule-preview-timezone {
background-color: var(--l1-background);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
}
}
}
}
.no-schedule {
min-height: 300px;
padding: 40px 12px;
svg {
color: var(--muted-foreground);
}
}
}
}
}
}
// Light mode styles

View File

@@ -5,7 +5,8 @@ import { ChevronDown, ChevronUp } from '@signozhq/icons';
import { useCreateAlertState } from '../context';
import EvaluationWindowPopover from './EvaluationWindowPopover';
import { getEvaluationWindowTypeText, getTimeframeText } from './utils';
import styles from './styles.module.scss';
import './styles.scss';
function EvaluationSettings(): JSX.Element {
const { evaluationWindow, setEvaluationWindow } = useCreateAlertState();
@@ -27,14 +28,13 @@ function EvaluationSettings(): JSX.Element {
}
trigger="click"
showArrow={false}
rootClassName="evaluation-window-popover-overlay"
>
<Button data-testid="evaluation-settings-button">
<div className={styles.evaluateAlertConditionsButtonLeft}>
<div className="evaluate-alert-conditions-button-left">
{getTimeframeText(evaluationWindow)}
</div>
<div className={styles.evaluateAlertConditionsButtonRight}>
<div className={styles.evaluateAlertConditionsButtonRightText}>
<div className="evaluate-alert-conditions-button-right">
<div className="evaluate-alert-conditions-button-right-text">
{getEvaluationWindowTypeText(evaluationWindow.windowType)}
</div>
{isEvaluationWindowPopoverOpen ? (
@@ -49,7 +49,7 @@ function EvaluationSettings(): JSX.Element {
return (
<div
className={styles.condensedEvaluationSettingsContainer}
className="condensed-evaluation-settings-container"
data-testid="condensed-evaluation-settings-container"
>
{popoverContent}

View File

@@ -12,7 +12,6 @@ import {
import TimeInput from '../TimeInput';
import { IEvaluationWindowDetailsProps } from '../types';
import { getCumulativeWindowTimeframeText } from '../utils';
import styles from '../styles.module.scss';
function EvaluationWindowDetails({
evaluationWindow,
@@ -118,12 +117,12 @@ function EvaluationWindowDetails({
if (isCurrentHour) {
return (
<div className={styles.evaluationWindowDetails}>
<div className="evaluation-window-details">
<Typography.Text>
{getCumulativeWindowDescription(evaluationWindow.timeframe)}
</Typography.Text>
<Typography.Text>{displayText}</Typography.Text>
<div className={styles.selectGroup}>
<div className="select-group">
<Typography.Text>STARTING AT MINUTE</Typography.Text>
<Select
options={currentHourOptions}
@@ -139,19 +138,19 @@ function EvaluationWindowDetails({
if (isCurrentDay) {
return (
<div className={styles.evaluationWindowDetails}>
<div className="evaluation-window-details">
<Typography.Text>
{getCumulativeWindowDescription(evaluationWindow.timeframe)}
</Typography.Text>
<Typography.Text>{displayText}</Typography.Text>
<div className={`${styles.selectGroup} ${styles.timeSelectGroup}`}>
<div className="select-group time-select-group">
<Typography.Text>STARTING AT</Typography.Text>
<TimeInput
value={evaluationWindow.startingAt.time}
onChange={handleTimeChange}
/>
</div>
<div className={styles.selectGroup}>
<div className="select-group">
<Typography.Text>SELECT TIMEZONE</Typography.Text>
<Select
options={TIMEZONE_DATA}
@@ -167,12 +166,12 @@ function EvaluationWindowDetails({
if (isCurrentMonth) {
return (
<div className={styles.evaluationWindowDetails}>
<div className="evaluation-window-details">
<Typography.Text>
{getCumulativeWindowDescription(evaluationWindow.timeframe)}
</Typography.Text>
<Typography.Text>{displayText}</Typography.Text>
<div className={styles.selectGroup}>
<div className="select-group">
<Typography.Text>STARTING ON DAY</Typography.Text>
<Select
options={currentMonthOptions}
@@ -182,14 +181,14 @@ function EvaluationWindowDetails({
data-testid="evaluation-window-details-starting-at-select"
/>
</div>
<div className={`${styles.selectGroup} ${styles.timeSelectGroup}`}>
<div className="select-group time-select-group">
<Typography.Text>STARTING AT</Typography.Text>
<TimeInput
value={evaluationWindow.startingAt.time}
onChange={handleTimeChange}
/>
</div>
<div className={styles.selectGroup}>
<div className="select-group">
<Typography.Text>SELECT TIMEZONE</Typography.Text>
<Select
options={TIMEZONE_DATA}
@@ -204,13 +203,13 @@ function EvaluationWindowDetails({
}
return (
<div className={styles.evaluationWindowDetails}>
<div className="evaluation-window-details">
<Typography.Text>
{getRollingWindowDescription(evaluationWindow.timeframe)}
</Typography.Text>
<Typography.Text>Specify custom duration</Typography.Text>
<Typography.Text>{displayText}</Typography.Text>
<div className={styles.selectGroup}>
<div className="select-group">
<Typography.Text>VALUE</Typography.Text>
<Input
name="value"
@@ -221,7 +220,7 @@ function EvaluationWindowDetails({
data-testid="evaluation-window-details-custom-rolling-window-duration-input"
/>
</div>
<div className={`${styles.selectGroup} ${styles.timeSelectGroup}`}>
<div className="select-group time-select-group">
<Typography.Text>UNIT</Typography.Text>
<Select
options={ADVANCED_OPTIONS_TIME_UNIT_OPTIONS}

View File

@@ -16,7 +16,6 @@ import {
} from '../types';
import EvaluationWindowDetails from './EvaluationWindowDetails';
import { useKeyboardNavigationForEvaluationWindowPopover } from './useKeyboardNavigation';
import styles from '../styles.module.scss';
function EvaluationWindowPopover({
evaluationWindow,
@@ -52,42 +51,34 @@ function EvaluationWindowPopover({
onChange: (value: string) => void,
sectionId: string,
): JSX.Element => (
<div
className={styles.evaluationWindowContentItem}
data-section-id={sectionId}
>
<Typography.Text className={styles.evaluationWindowContentItemLabel}>
<div className="evaluation-window-content-item" data-section-id={sectionId}>
<Typography.Text className="evaluation-window-content-item-label">
{label}
</Typography.Text>
<div className={styles.evaluationWindowContentList}>
{contentOptions.map((option, index) => {
const isActive = currentValue === option.value;
return (
<div
className={classNames(styles.evaluationWindowContentListItem, {
[styles.evaluationWindowContentListItemActive]: isActive,
})}
key={option.value}
role="button"
tabIndex={0}
data-value={option.value}
data-section-id={sectionId}
data-testid={`${sectionId}-option-${option.value}`}
data-active={isActive}
onClick={(): void => onChange(option.value)}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onChange(option.value);
}
}}
ref={index === 0 ? firstItemRef : undefined}
>
<Typography.Text>{option.label}</Typography.Text>
{isActive && <Check size={12} />}
</div>
);
})}
<div className="evaluation-window-content-list">
{contentOptions.map((option, index) => (
<div
className={classNames('evaluation-window-content-list-item', {
active: currentValue === option.value,
})}
key={option.value}
role="button"
tabIndex={0}
data-value={option.value}
data-section-id={sectionId}
onClick={(): void => onChange(option.value)}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onChange(option.value);
}
}}
ref={index === 0 ? firstItemRef : undefined}
>
<Typography.Text>{option.label}</Typography.Text>
{currentValue === option.value && <Check size={12} />}
</div>
))}
</div>
</div>
);
@@ -103,7 +94,7 @@ function EvaluationWindowPopover({
);
}
return (
<div className={styles.selectionContent}>
<div className="selection-content">
<Typography.Text>
{getRollingWindowDescription(evaluationWindow.timeframe)}
</Typography.Text>
@@ -117,7 +108,7 @@ function EvaluationWindowPopover({
!evaluationWindow.timeframe
) {
return (
<div className={styles.selectionContent}>
<div className="selection-content">
<Typography.Text>
{getCumulativeWindowDescription(evaluationWindow.timeframe)}
</Typography.Text>
@@ -136,12 +127,12 @@ function EvaluationWindowPopover({
return (
<div
className={styles.evaluationWindowPopover}
className="evaluation-window-popover"
ref={containerRef}
role="menu"
aria-label="Evaluation window options"
>
<div className={styles.evaluationWindowContent}>
<div className="evaluation-window-content">
{renderEvaluationWindowContent(
'EVALUATION WINDOW',
EVALUATION_WINDOW_TYPE,

View File

@@ -1,54 +0,0 @@
.timeInputContainer {
display: flex;
align-items: center;
gap: 0;
}
// Compound + descendant selector keeps specificity above
// parent `.selectGroup :global(.ant-input)` override so the
// 40px field width is not clobbered to 60%.
.timeInputContainer :global(.ant-input).timeInputField {
width: 40px;
height: 32px;
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
font-family: 'Space Mono', monospace;
font-size: 13px;
font-weight: 600;
text-align: center;
border-radius: 4px;
&::placeholder {
color: var(--l2-foreground);
font-family: 'Space Mono', monospace;
}
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
outline: none;
}
&:disabled {
background-color: var(--l2-background);
color: var(--l2-foreground);
cursor: not-allowed;
&:hover {
border-color: var(--l1-border);
}
}
}
.timeInputSeparator {
color: var(--l2-foreground);
font-size: 13px;
font-weight: 600;
margin: 0 4px;
user-select: none;
}

View File

@@ -0,0 +1,51 @@
.time-input-container {
display: flex;
align-items: center;
gap: 0;
.time-input-field {
width: 40px;
height: 32px;
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
font-family: 'Space Mono', monospace;
font-size: 13px;
font-weight: 600;
text-align: center;
border-radius: 4px;
&::placeholder {
color: var(--l2-foreground);
font-family: 'Space Mono', monospace;
}
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
outline: none;
}
&:disabled {
background-color: var(--l2-background);
color: var(--l2-foreground);
cursor: not-allowed;
&:hover {
border-color: var(--l1-border);
}
}
}
.time-input-separator {
color: var(--l2-foreground);
font-size: 13px;
font-weight: 600;
margin: 0 4px;
user-select: none;
}
}

View File

@@ -1,7 +1,6 @@
import { Input } from '@signozhq/ui/input';
import React, { useEffect, useState } from 'react';
import styles from './TimeInput.module.scss';
import { Input } from '@signozhq/ui/input';
import './TimeInput.scss';
export interface TimeInputProps {
value?: string; // Format: "HH:MM:SS"
@@ -145,10 +144,7 @@ function TimeInput({
};
return (
<div
data-testid="time-input"
className={`${styles.timeInputContainer} ${className}`.trim()}
>
<div data-testid="time-input" className={`time-input-container ${className}`}>
<Input
data-field="hours"
value={hours}
@@ -157,11 +153,11 @@ function TimeInput({
onKeyDown={(e): void => handleKeyDown(e, 'hours')}
disabled={disabled}
maxLength={2}
className={styles.timeInputField}
className="time-input-field"
placeholder="00"
data-testid="time-input-hours"
/>
<span className={styles.timeInputSeparator}>:</span>
<span className="time-input-separator">:</span>
<Input
data-field="minutes"
value={minutes}
@@ -170,11 +166,11 @@ function TimeInput({
onKeyDown={(e): void => handleKeyDown(e, 'minutes')}
disabled={disabled}
maxLength={2}
className={styles.timeInputField}
className="time-input-field"
placeholder="00"
data-testid="time-input-minutes"
/>
<span className={styles.timeInputSeparator}>:</span>
<span className="time-input-separator">:</span>
<Input
data-field="seconds"
value={seconds}
@@ -183,7 +179,7 @@ function TimeInput({
onKeyDown={(e): void => handleKeyDown(e, 'seconds')}
disabled={disabled}
maxLength={2}
className={styles.timeInputField}
className="time-input-field"
placeholder="00"
data-testid="time-input-seconds"
/>

View File

@@ -13,12 +13,9 @@ jest.spyOn(alertState, 'useCreateAlertState').mockReturnValue(
const ALERT_WHEN_DATA_STOPS_COMING_TEXT = 'Alert when data stops coming';
const MINIMUM_DATA_REQUIRED_TEXT = 'Minimum data required';
// const ACCOUNT_FOR_DATA_DELAY_TEXT = 'Account for data delay';
const ACCOUNT_FOR_DATA_DELAY_TEXT = 'Account for data delay';
const ADVANCED_OPTION_ITEM_CLASS = '.advanced-option-item';
const SWITCH_ROLE_SELECTOR = '[role="switch"]';
const SEND_NOTIFICATION_TEST_ID =
'send-notification-if-data-is-missing-container';
const ENFORCE_MINIMUM_DATAPOINTS_TEST_ID =
'enforce-minimum-datapoints-container';
describe('AdvancedOptions', () => {
it('should render evaluation cadence and the advanced options minimized by default', () => {
@@ -67,9 +64,9 @@ describe('AdvancedOptions', () => {
const collapse = screen.getByRole('button', { name: /ADVANCED OPTIONS/i });
fireEvent.click(collapse);
const alertWhenDataStopsComingContainer = screen.getByTestId(
SEND_NOTIFICATION_TEST_ID,
);
const alertWhenDataStopsComingContainer = screen
.getByText(ALERT_WHEN_DATA_STOPS_COMING_TEXT)
.closest(ADVANCED_OPTION_ITEM_CLASS);
const alertWhenDataStopsComingSwitch =
alertWhenDataStopsComingContainer?.querySelector(
SWITCH_ROLE_SELECTOR,
@@ -97,9 +94,9 @@ describe('AdvancedOptions', () => {
const collapse = screen.getByRole('button', { name: /ADVANCED OPTIONS/i });
fireEvent.click(collapse);
const minimumDataRequiredContainer = screen.getByTestId(
ENFORCE_MINIMUM_DATAPOINTS_TEST_ID,
);
const minimumDataRequiredContainer = screen
.getByText(MINIMUM_DATA_REQUIRED_TEXT)
.closest(ADVANCED_OPTION_ITEM_CLASS);
const minimumDataRequiredSwitch = minimumDataRequiredContainer?.querySelector(
SWITCH_ROLE_SELECTOR,
) as HTMLElement;
@@ -119,17 +116,15 @@ describe('AdvancedOptions', () => {
});
});
// TODO: Update when account for data delay is implemented - will need a data-testid
it.skip('"Account for data delay" works as expected', () => {
render(<AdvancedOptions />);
const collapse = screen.getByRole('button', { name: /ADVANCED OPTIONS/i });
fireEvent.click(collapse);
// This test needs a data-testid on the account for data delay component
const accountForDataDelayContainer = screen.getByTestId(
'account-for-data-delay-container',
);
const accountForDataDelayContainer = screen
.getByText(ACCOUNT_FOR_DATA_DELAY_TEXT)
.closest(ADVANCED_OPTION_ITEM_CLASS);
const accountForDataDelaySwitch = accountForDataDelayContainer?.querySelector(
SWITCH_ROLE_SELECTOR,
) as HTMLElement;

View File

@@ -16,7 +16,7 @@ jest.spyOn(alertState, 'useCreateAlertState').mockReturnValue(
const mockSetIsEvaluationCadenceDetailsVisible = jest.fn();
const mockSetIsPreviewVisible = jest.fn();
const EDIT_CUSTOM_SCHEDULE_TEST_ID = 'edit-custom-schedule';
const EDIT_CUSTOM_SCHEDULE_TEST_ID = '.edit-custom-schedule';
describe('EditCustomSchedule', () => {
it('should render the correct display text for custom mode with daily occurrence', () => {
@@ -47,7 +47,9 @@ describe('EditCustomSchedule', () => {
);
// Use textContent to verify the complete text across multiple Typography components
const container = screen.getByTestId(EDIT_CUSTOM_SCHEDULE_TEST_ID);
const container = screen
.getByText('Every')
.closest(EDIT_CUSTOM_SCHEDULE_TEST_ID);
expect(container).toHaveTextContent('EveryDayat00:00:00');
});
@@ -79,7 +81,9 @@ describe('EditCustomSchedule', () => {
/>,
);
const container = screen.getByTestId(EDIT_CUSTOM_SCHEDULE_TEST_ID);
const container = screen
.getByText('Every')
.closest(EDIT_CUSTOM_SCHEDULE_TEST_ID);
expect(container).toHaveTextContent(
'EveryWeekonMonday, Tuesday, Wednesday, Thursday, Fridayat00:00:00',
);
@@ -113,7 +117,9 @@ describe('EditCustomSchedule', () => {
/>,
);
const container = screen.getByTestId(EDIT_CUSTOM_SCHEDULE_TEST_ID);
const container = screen
.getByText('Every')
.closest(EDIT_CUSTOM_SCHEDULE_TEST_ID);
expect(container).toHaveTextContent('EveryMonthon1at00:00:00');
});

View File

@@ -12,15 +12,12 @@ const mockEvaluationWindow: EvaluationWindowState =
createMockEvaluationWindowState();
const mockSetEvaluationWindow = jest.fn();
const EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS =
'.evaluation-window-content-list-item';
const EVALUATION_WINDOW_DETAILS_TEST_ID = 'evaluation-window-details';
const ENTER_VALUE_PLACEHOLDER = 'Enter value';
const EVALUATION_WINDOW_TEXT = 'EVALUATION WINDOW';
// Test IDs for window type and timeframe options
const WINDOW_TYPE_ROLLING_TEST_ID = 'window-type-option-rolling';
const WINDOW_TYPE_CUMULATIVE_TEST_ID = 'window-type-option-cumulative';
const TIMEFRAME_LAST_5_MINUTES_TEST_ID = 'timeframe-option-5m0s';
const TIMEFRAME_CURRENT_HOUR_TEST_ID = 'timeframe-option-currentHour';
const LAST_5_MINUTES_TEXT = 'Last 5 minutes';
jest.mock('../EvaluationWindowPopover/EvaluationWindowDetails', () => ({
__esModule: true,
@@ -52,11 +49,15 @@ describe('EvaluationWindowPopover', () => {
EVALUATION_WINDOW_TYPE.forEach((option) => {
expect(screen.getByText(option.label)).toBeInTheDocument();
});
const rollingItem = screen.getByTestId(WINDOW_TYPE_ROLLING_TEST_ID);
expect(rollingItem).toHaveAttribute('data-active', 'true');
const rollingItem = screen
.getByText('Rolling')
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
expect(rollingItem).toHaveClass('active');
const cumulativeItem = screen.getByTestId(WINDOW_TYPE_CUMULATIVE_TEST_ID);
expect(cumulativeItem).toHaveAttribute('data-active', 'false');
const cumulativeItem = screen
.getByText('Cumulative')
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
expect(cumulativeItem).not.toHaveClass('active');
});
it('should render all window type options with cumulative selected', () => {
@@ -72,10 +73,14 @@ describe('EvaluationWindowPopover', () => {
expect(screen.getByText(option.label)).toBeInTheDocument();
});
const cumulativeItem = screen.getByTestId(WINDOW_TYPE_CUMULATIVE_TEST_ID);
expect(cumulativeItem).toHaveAttribute('data-active', 'true');
const rollingItem = screen.getByTestId(WINDOW_TYPE_ROLLING_TEST_ID);
expect(rollingItem).toHaveAttribute('data-active', 'false');
const cumulativeItem = screen
.getByText('Cumulative')
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
expect(cumulativeItem).toHaveClass('active');
const rollingItem = screen
.getByText('Rolling')
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
expect(rollingItem).not.toHaveClass('active');
});
it('should render all timeframe options in rolling mode with last 5 minutes selected by default', () => {
@@ -88,8 +93,10 @@ describe('EvaluationWindowPopover', () => {
EVALUATION_WINDOW_TIMEFRAME.rolling.forEach((option) => {
expect(screen.getByText(option.label)).toBeInTheDocument();
});
const last5MinutesItem = screen.getByTestId(TIMEFRAME_LAST_5_MINUTES_TEST_ID);
expect(last5MinutesItem).toHaveAttribute('data-active', 'true');
const last5MinutesItem = screen
.getByText(LAST_5_MINUTES_TEXT)
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
expect(last5MinutesItem).toHaveClass('active');
});
it('should render all timeframe options in cumulative mode with current hour selected by default', () => {
@@ -105,8 +112,10 @@ describe('EvaluationWindowPopover', () => {
EVALUATION_WINDOW_TIMEFRAME.cumulative.forEach((option) => {
expect(screen.getByText(option.label)).toBeInTheDocument();
});
const currentHourItem = screen.getByTestId(TIMEFRAME_CURRENT_HOUR_TEST_ID);
expect(currentHourItem).toHaveAttribute('data-active', 'true');
const currentHourItem = screen
.getByText('Current hour')
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
expect(currentHourItem).toHaveClass('active');
});
it('renders help text in details section for rolling mode with non-custom timeframe', () => {
@@ -178,11 +187,15 @@ describe('EvaluationWindowPopover', () => {
/>,
);
const rollingItem = screen.getByTestId(WINDOW_TYPE_ROLLING_TEST_ID);
const rollingItem = screen
.getByText('Rolling')
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
rollingItem?.focus();
fireEvent.keyDown(rollingItem, { key: 'ArrowDown' });
const cumulativeItem = screen.getByTestId(WINDOW_TYPE_CUMULATIVE_TEST_ID);
const cumulativeItem = screen
.getByText('Cumulative')
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS);
expect(cumulativeItem).toHaveFocus();
});
@@ -194,11 +207,15 @@ describe('EvaluationWindowPopover', () => {
/>,
);
const cumulativeItem = screen.getByTestId(WINDOW_TYPE_CUMULATIVE_TEST_ID);
const cumulativeItem = screen
.getByText('Cumulative')
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
cumulativeItem?.focus();
fireEvent.keyDown(cumulativeItem, { key: 'ArrowUp' });
const rollingItem = screen.getByTestId(WINDOW_TYPE_ROLLING_TEST_ID);
const rollingItem = screen
.getByText('Rolling')
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS);
expect(rollingItem).toHaveFocus();
});
@@ -210,11 +227,15 @@ describe('EvaluationWindowPopover', () => {
/>,
);
const rollingItem = screen.getByTestId(WINDOW_TYPE_ROLLING_TEST_ID);
const rollingItem = screen
.getByText('Rolling')
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
rollingItem?.focus();
fireEvent.keyDown(rollingItem, { key: 'ArrowRight' });
const timeframeItem = screen.getByTestId(TIMEFRAME_LAST_5_MINUTES_TEST_ID);
const timeframeItem = screen
.getByText(LAST_5_MINUTES_TEXT)
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS);
expect(timeframeItem).toHaveFocus();
});
@@ -226,11 +247,15 @@ describe('EvaluationWindowPopover', () => {
/>,
);
const timeframeItem = screen.getByTestId(TIMEFRAME_LAST_5_MINUTES_TEST_ID);
const timeframeItem = screen
.getByText(LAST_5_MINUTES_TEXT)
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
timeframeItem?.focus();
fireEvent.keyDown(timeframeItem, { key: 'ArrowLeft' });
const rollingItem = screen.getByTestId(WINDOW_TYPE_ROLLING_TEST_ID);
const rollingItem = screen
.getByText('Rolling')
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS);
expect(rollingItem).toHaveFocus();
});
@@ -242,7 +267,9 @@ describe('EvaluationWindowPopover', () => {
/>,
);
const cumulativeItem = screen.getByTestId(WINDOW_TYPE_CUMULATIVE_TEST_ID);
const cumulativeItem = screen
.getByText('Cumulative')
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
cumulativeItem?.focus();
fireEvent.keyDown(cumulativeItem, { key: 'Enter' });
@@ -260,7 +287,9 @@ describe('EvaluationWindowPopover', () => {
/>,
);
const cumulativeItem = screen.getByTestId(WINDOW_TYPE_CUMULATIVE_TEST_ID);
const cumulativeItem = screen
.getByText('Cumulative')
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
cumulativeItem?.focus();
fireEvent.keyDown(cumulativeItem, { key: ' ' });

View File

@@ -1,302 +0,0 @@
.evaluationSettingsContainer {
margin: var(--spacing-8);
}
.evaluateAlertConditionsContainer {
display: flex;
align-items: center;
gap: var(--spacing-8);
background-color: var(--l2-background);
padding: var(--spacing-8);
border-radius: 4px;
border: 1px solid var(--l1-border);
margin-bottom: var(--spacing-8);
:global(.ant-typography) {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
}
:global(.ant-btn) {
display: flex;
align-items: center;
justify-content: space-between;
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
}
}
.evaluateAlertConditionsSeparator {
flex: 1;
height: 1px;
border-top: 1px dashed var(--l1-border);
}
.evaluateAlertConditionsButtonLeft {
color: var(--l2-foreground);
font-size: 12px;
padding-right: var(--spacing-8);
}
.evaluateAlertConditionsButtonRight {
display: flex;
align-items: center;
color: var(--muted-foreground);
gap: var(--spacing-4);
}
.evaluateAlertConditionsButtonRightText {
font-size: 12px;
font-weight: var(--font-weight-medium);
background-color: var(--l2-background);
padding: 1px var(--spacing-2);
}
.advancedOptionsContainer {
:global(.ant-collapse) {
:global(.ant-collapse-item) {
:global(.ant-collapse-header) {
background-color: var(--card);
border: 1px solid var(--l1-border);
:global(.ant-collapse-header-text) {
color: var(--muted-foreground);
font-family: Inter;
}
}
:global(.ant-collapse-content) {
:global(.ant-collapse-content-box) {
background-color: var(--card);
}
}
}
}
}
.condensedEvaluationSettingsContainer {
:global(.ant-btn) {
display: flex;
align-items: center;
min-width: 240px;
width: auto;
justify-content: space-between;
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
}
}
.evaluateAlertConditionsButtonLeft {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-small);
flex-shrink: 0;
}
.evaluateAlertConditionsButtonRight {
display: flex;
align-items: center;
color: var(--l2-foreground);
gap: var(--spacing-4);
flex-shrink: 0;
}
.evaluateAlertConditionsButtonRightText {
font-size: var(--periscope-font-size-small);
font-weight: 500;
background-color: var(--l1-border);
padding: 1px 4px;
}
:global(.evaluation-window-popover-overlay) {
:global(.ant-popover-arrow) {
display: none !important;
}
:global(.ant-popover-content) {
background-color: var(--card);
border: 1px solid var(--l1-border);
border-radius: 4px;
padding: 0;
margin: 10px;
}
:global(.ant-popover-inner) {
background-color: var(--l2-background);
border: none;
padding: 0;
}
}
.evaluationWindowPopover {
min-width: 500px;
}
.evaluationWindowContent {
display: flex;
}
.evaluationWindowContentItem {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
border-right: 1px solid var(--l1-border);
padding: var(--spacing-6) var(--spacing-8);
min-width: 250px;
min-height: 300px;
}
.evaluationWindowContentItemLabel {
color: var(--muted-foreground);
font-size: var(--periscope-font-size-small);
line-height: 18px;
font-weight: var(--font-weight-medium);
}
.evaluationWindowContentList {
display: flex;
flex-direction: column;
}
.evaluationWindowContentListItem {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 calc(-1 * var(--spacing-8));
padding: var(--spacing-2) var(--spacing-8);
:global(.ant-typography) {
color: var(--muted-foreground);
font-weight: 400;
}
&:hover {
cursor: pointer;
background-color: var(--l1-background);
}
}
.evaluationWindowContentListItemActive {
background-color: var(--l1-background);
border-left: 2px solid var(--bg-robin-500);
:global(.ant-typography) {
font-weight: var(--font-weight-medium);
color: var(--l1-foreground);
}
}
.selectionContent {
padding: var(--spacing-8);
display: flex;
flex-direction: column;
gap: var(--spacing-8);
width: 400px;
:global(.ant-typography) {
color: var(--muted-foreground);
}
:global(.ant-btn) {
width: fit-content;
}
}
.evaluationWindowFooter {
display: flex;
justify-content: flex-end;
gap: var(--spacing-4);
background-color: var(--l2-background);
border-top: 1px solid var(--l1-border);
padding: var(--spacing-8);
:global(.ant-btn) {
background-color: var(--l3-background);
border: 1px solid var(--l1-border);
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
}
}
.evaluationWindowDetails {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
width: 400px;
min-height: 300px;
padding: var(--spacing-8);
:global(.ant-typography) {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-medium);
}
:global(.ant-select) {
width: 60%;
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
height: 32px;
:global(.ant-select-selector) {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
height: 32px;
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
}
}
&:hover {
border-color: var(--l1-border);
}
}
}
.selectGroup {
display: flex;
flex-direction: column;
gap: 2px;
:global(.ant-typography) {
color: var(--l3-foreground);
font-size: var(--periscope-font-size-small);
line-height: 18px;
font-weight: var(--font-weight-medium);
}
:global(.ant-input) {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
height: 32px;
width: 60%;
}
}
.timeSelectGroup {
:global(.ant-input-group) {
flex-direction: row;
gap: var(--spacing-4);
:global(.ant-select) {
width: 40px;
:global(.ant-select-selector) {
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
}
}
}
}
}

View File

@@ -0,0 +1,266 @@
.evaluation-settings-container {
margin: 16px;
.evaluate-alert-conditions-container {
display: flex;
align-items: center;
gap: 16px;
background-color: var(--l2-background);
padding: 16px;
border-radius: 4px;
border: 1px solid var(--l1-border);
margin-bottom: 16px;
.ant-typography {
color: var(--l2-foreground);
font-size: 13px;
}
.evaluate-alert-conditions-separator {
flex: 1;
height: 1px;
border-top: 1px dashed var(--l1-border);
}
.ant-btn {
display: flex;
align-items: center;
justify-content: space-between;
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
.evaluate-alert-conditions-button-left {
color: var(--l2-foreground);
font-size: 12px;
padding-right: 16px;
}
.evaluate-alert-conditions-button-right {
display: flex;
align-items: center;
color: var(--muted-foreground);
gap: 8px;
.evaluate-alert-conditions-button-right-text {
font-size: 12px;
font-weight: 500;
background-color: var(--l2-background);
padding: 1px 4px;
}
}
}
}
}
.advanced-options-container {
.ant-collapse {
.ant-collapse-item {
.ant-collapse-header {
background-color: var(--card);
border: 1px solid var(--l1-border);
.ant-collapse-header-text {
color: var(--muted-foreground);
font-family: Inter;
}
}
.ant-collapse-content {
.ant-collapse-content-box {
background-color: var(--card);
}
}
}
}
}
.ant-popover-arrow {
display: none !important;
}
.ant-popover-content {
background-color: var(--card);
border: 1px solid var(--l1-border);
border-radius: 4px;
padding: 0;
margin: 10px;
.ant-popover-inner {
background-color: var(--l2-background);
border: none;
padding: 0;
.evaluation-window-popover {
min-width: 500px;
.evaluation-window-content {
display: flex;
.evaluation-window-content-item {
display: flex;
flex-direction: column;
gap: 8px;
border-right: 1px solid var(--l1-border);
padding: 12px 16px;
min-width: 250px;
min-height: 300px;
.evaluation-window-content-item-label {
color: var(--muted-foreground);
font-size: 11px;
line-height: 18px;
font-weight: 500;
}
.evaluation-window-content-list {
display: flex;
flex-direction: column;
.evaluation-window-content-list-item {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 -16px;
padding: 4px 16px;
.ant-typography {
color: var(--muted-foreground);
font-weight: 400;
}
&.active {
background-color: var(--l1-background);
border-left: 2px solid var(--bg-robin-500);
.ant-typography {
font-weight: 500;
color: var(--l1-foreground);
}
}
&:hover {
cursor: pointer;
background-color: var(--l1-background);
}
}
}
}
.selection-content {
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
width: 400px;
.ant-typography {
color: var(--muted-foreground);
}
.ant-btn {
width: fit-content;
}
}
}
.evaluation-window-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
background-color: var(--l2-background);
border-top: 1px solid var(--l1-border);
padding: 16px;
}
.ant-btn {
background-color: var(--l3-background);
border: 1px solid var(--l1-border);
color: var(--l2-foreground);
font-size: 13px;
}
}
}
}
.evaluation-window-details {
display: flex;
flex-direction: column;
gap: 16px;
width: 400px;
min-height: 300px;
padding: 16px;
.select-group {
display: flex;
flex-direction: column;
gap: 2px;
.ant-typography {
color: var(--l3-foreground);
font-size: 11px;
line-height: 18px;
font-weight: 500;
}
}
.time-select-group {
.ant-input-group {
flex-direction: row;
gap: 8px;
.ant-select {
width: 40px;
.ant-select-selector {
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
}
}
}
}
}
.ant-typography {
color: var(--l2-foreground);
font-size: 13px;
font-weight: 500;
}
.ant-select {
width: 60%;
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
height: 32px;
.ant-select-selector {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
height: 32px;
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
}
}
&:hover {
border-color: var(--l1-border);
}
}
.select-group .ant-input:not(.time-input-field) {
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
height: 32px;
width: 60%;
}
}

View File

@@ -1,19 +0,0 @@
.footer {
position: fixed;
bottom: 0;
left: 63px;
right: 0;
background-color: var(--l1-background);
border-top: 1px solid var(--l1-border);
padding: var(--spacing-6);
z-index: 1000;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.buttonGroup {
display: flex;
gap: var(--spacing-6);
}

View File

@@ -19,7 +19,7 @@ import {
validateCreateAlertState,
} from './utils';
import styles from './Footer.module.scss';
import './styles.scss';
import {
invalidateGetRuleByID,
invalidateListRules,
@@ -243,7 +243,7 @@ function Footer(): JSX.Element {
]);
return (
<div className={styles.footer}>
<div className="create-alert-v2-footer">
<Button
variant="solid"
color="secondary"
@@ -252,7 +252,7 @@ function Footer(): JSX.Element {
>
<X size={14} /> Discard
</Button>
<div className={styles.buttonGroup}>
<div className="button-group">
{testAlertButton}
{saveAlertButton}
</div>

View File

@@ -0,0 +1,31 @@
.create-alert-v2-footer {
position: fixed;
bottom: 0;
left: 63px;
right: 0;
background-color: var(--l1-background);
border-top: 1px solid var(--l1-border);
padding: 12px;
z-index: 1000;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
.button-group {
display: flex;
gap: 12px;
}
.ant-btn {
display: flex;
align-items: center;
gap: 8px;
}
.ant-btn-default {
background-color: var(--l1-background);
border: 1px solid var(--l1-border);
color: var(--l2-foreground);
}
}

View File

@@ -7,8 +7,6 @@ import { Info } from '@signozhq/icons';
import { ALL_SELECTED_VALUE } from '../constants';
import { useCreateAlertState } from '../context';
import styles from './NotificationSettings.module.scss';
function MultipleNotifications(): JSX.Element {
const { notificationSettings, setNotificationSettings } =
useCreateAlertState();
@@ -101,7 +99,7 @@ function MultipleNotifications(): JSX.Element {
data-testid="multiple-notifications-select"
/>
{isMultipleNotificationsEnabled && (
<Typography.Text className={styles.multipleNotificationsSelectDescription}>
<Typography.Text className="multiple-notifications-select-description">
{groupByDescription}
</Typography.Text>
)}
@@ -124,15 +122,15 @@ function MultipleNotifications(): JSX.Element {
]);
return (
<div className={styles.multipleNotificationsContainer}>
<div className={styles.multipleNotificationsHeader}>
<Typography.Text className={styles.multipleNotificationsHeaderTitle}>
<div className="multiple-notifications-container">
<div className="multiple-notifications-header">
<Typography.Text className="multiple-notifications-header-title">
Group alerts by{' '}
<Tooltip title="Group similar alerts together to reduce notification volume. Leave empty to combine all matching alerts into one notification without grouping.">
<Info size={16} />
</Tooltip>
</Typography.Text>
<Typography.Text className={styles.multipleNotificationsHeaderDescription}>
<Typography.Text className="multiple-notifications-header-description">
Combine alerts with the same field values into a single notification.
</Typography.Text>
</div>

View File

@@ -4,8 +4,6 @@ import { Info } from '@signozhq/icons';
import { useCreateAlertState } from '../context';
import styles from './NotificationSettings.module.scss';
function NotificationMessage(): JSX.Element {
const { notificationSettings, setNotificationSettings } =
useCreateAlertState();
@@ -52,21 +50,21 @@ function NotificationMessage(): JSX.Element {
// );
return (
<div className={styles.notificationMessageContainer}>
<div className={styles.notificationMessageHeader}>
<div className={styles.notificationMessageHeaderContent}>
<Typography.Text className={styles.notificationMessageHeaderTitle}>
<div className="notification-message-container">
<div className="notification-message-header">
<div className="notification-message-header-content">
<Typography.Text className="notification-message-header-title">
Notification Message
<Tooltip title="Customize the message content sent in alert notifications. Template variables like {{alertname}}, {{value}}, and {{threshold}} will be replaced with actual values when the alert fires.">
<Info size={16} />
</Tooltip>
</Typography.Text>
<Typography.Text className={styles.notificationMessageHeaderDescription}>
<Typography.Text className="notification-message-header-description">
Custom message content for alert notifications. Use template variables to
include dynamic information.
</Typography.Text>
</div>
<div className={styles.notificationMessageHeaderActions}>
<div className="notification-message-header-actions">
{/* TODO: Add back when the functionality is implemented */}
{/* <Popover content={templateVariableContent}>
<Button type="text">

View File

@@ -1,262 +0,0 @@
.notificationSettingsContainer {
display: flex;
flex-direction: column;
margin: 0 var(--spacing-8);
}
.notificationMessageContainer {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
margin-top: -8px;
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
padding: var(--spacing-8);
textarea {
height: 150px;
background: var(--l2-background);
border: 1px solid var(--l1-border);
border-radius: 4px;
color: var(--l2-foreground) !important;
font-family: Inter;
font-size: var(--periscope-font-size-base);
}
}
.notificationMessageHeader {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--spacing-8);
}
.notificationMessageHeaderContent {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
.notificationMessageHeaderTitle {
--typography-text-display: flex;
gap: var(--spacing-4);
align-items: center;
color: var(--l1-foreground);
font-family: Inter;
font-size: var(--periscope-font-size-base);
font-weight: 500;
}
.notificationMessageHeaderDescription {
color: var(--l2-foreground);
font-family: Inter;
font-size: var(--periscope-font-size-base);
font-weight: 400;
}
.notificationMessageHeaderActions {
:global(.ant-btn) {
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
color: var(--bg-robin-400);
}
}
.notificationSettingsContent {
display: flex;
flex-direction: column;
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
padding: var(--spacing-8);
margin-top: var(--spacing-8);
}
.repeatNotificationsInput {
display: flex;
align-items: center;
gap: var(--spacing-4);
:global(.ant-input) {
width: 120px;
border: 1px solid var(--l1-border);
}
:global(.ant-select) {
:global(.ant-select-selector) {
width: 120px;
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
}
}
}
:global(.ant-select-multiple) {
:global(.ant-select-selector) {
width: 200px;
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
}
}
}
}
.multipleNotificationsContainer {
display: flex;
padding: 4px var(--spacing-8) var(--spacing-8) var(--spacing-8);
border-bottom: 1px solid var(--l1-border);
justify-content: space-between;
:global(.ant-select) {
width: 300px;
}
}
.multipleNotificationsHeader {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
:global(.ant-typography) {
--typography-text-display: flex;
gap: 4px;
align-items: center;
}
}
.multipleNotificationsHeaderTitle {
color: var(--l1-foreground);
font-family: Inter;
font-size: var(--periscope-font-size-base);
font-weight: 500;
--typography-text-display: flex;
align-items: center;
gap: var(--spacing-4);
}
.multipleNotificationsHeaderDescription {
color: var(--l2-foreground);
font-family: Inter;
font-size: var(--periscope-font-size-base);
font-weight: 400;
}
.multipleNotificationsSelectDescription {
font-size: var(--periscope-font-size-small);
color: var(--l2-foreground);
margin-top: 4px;
--typography-text-display: block;
}
.reNotificationContainer {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
padding: var(--spacing-8);
margin-top: var(--spacing-8);
}
.advancedOptionItem {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--spacing-8);
}
.advancedOptionItemLeftContent {
display: flex;
flex-direction: column;
gap: var(--spacing-3);
}
.advancedOptionItemTitle {
color: var(--l2-foreground);
font-family: Inter;
font-size: var(--periscope-font-size-base);
font-weight: 500;
}
.advancedOptionItemDescription {
color: var(--muted-foreground);
font-family: Inter;
font-size: var(--periscope-font-size-base);
font-weight: 400;
}
.borderBottom {
border-bottom: 1px solid var(--l1-border);
width: 100%;
margin-left: -16px;
margin-right: -32px;
}
.reNotificationCondition {
display: flex;
align-items: center;
gap: var(--spacing-4);
flex-wrap: nowrap;
:global(.ant-typography) {
font-size: var(--periscope-font-size-base);
font-weight: 400;
color: var(--l2-foreground);
white-space: nowrap;
}
:global(.ant-select) {
width: 200px;
height: 32px;
flex-shrink: 0;
:global(.ant-select-selector) {
border: 1px solid var(--l1-border);
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
}
}
:global(.ant-input) {
width: 200px;
flex-shrink: 0;
border: 1px solid var(--l1-border);
}
}
.templateVariableContent {
padding: var(--spacing-8);
display: flex;
flex-direction: column;
gap: 2px;
}
.templateVariableContentItem {
display: flex;
gap: var(--spacing-4);
align-items: center;
code {
background-color: var(--l1-background);
color: var(--l2-foreground);
padding: 2px 4px;
}
}

View File

@@ -12,14 +12,14 @@ import Stepper from '../Stepper';
import MultipleNotifications from './MultipleNotifications';
import NotificationMessage from './NotificationMessage';
import styles from './NotificationSettings.module.scss';
import './styles.scss';
function NotificationSettings(): JSX.Element {
const { notificationSettings, setNotificationSettings } =
useCreateAlertState();
const repeatNotificationsInput = (
<div className={styles.repeatNotificationsInput}>
<div className="repeat-notifications-input">
<Typography.Text>Every</Typography.Text>
<Input
value={notificationSettings.reNotification.value}
@@ -81,10 +81,10 @@ function NotificationSettings(): JSX.Element {
);
return (
<div className={styles.notificationSettingsContainer}>
<div className="notification-settings-container">
<Stepper stepNumber={3} label="Notification settings" />
<NotificationMessage />
<div className={styles.notificationSettingsContent}>
<div className="notification-settings-content">
<MultipleNotifications />
<AdvancedOptionItem
title="Repeat notifications"

View File

@@ -0,0 +1,261 @@
.notification-settings-container {
display: flex;
flex-direction: column;
margin: 0 16px;
.notification-message-container {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: -8px;
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
padding: 16px;
.notification-message-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
.notification-message-header-content {
display: flex;
flex-direction: column;
gap: 8px;
.notification-message-header-title {
display: flex;
gap: 8px;
align-items: center;
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-weight: 500;
}
.notification-message-header-description {
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
font-weight: 400;
}
}
.notification-message-header-actions {
.ant-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
color: var(--bg-robin-400);
}
}
}
textarea {
height: 150px;
background: var(--l2-background);
border: 1px solid var(--l1-border);
border-radius: 4px;
color: var(--l2-foreground) !important;
font-family: Inter;
font-size: 13px;
}
}
.notification-settings-content {
display: flex;
flex-direction: column;
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
padding: 16px;
margin-top: 16px;
.repeat-notifications-input {
display: flex;
align-items: center;
gap: 8px;
.ant-input {
width: 120px;
border: 1px solid var(--l1-border);
}
.ant-select {
.ant-select-selector {
width: 120px;
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
}
}
}
.ant-select-multiple {
.ant-select-selector {
width: 200px;
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
}
}
}
}
.multiple-notifications-container {
display: flex;
padding: 4px 16px 16px 16px;
border-bottom: 1px solid var(--l1-border);
justify-content: space-between;
.multiple-notifications-header {
display: flex;
flex-direction: column;
gap: 8px;
.ant-typography {
display: flex;
gap: 4px;
align-items: center;
}
.multiple-notifications-header-title {
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.multiple-notifications-header-description {
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
font-weight: 400;
}
}
.ant-select {
width: 300px;
}
.multiple-notifications-select-description {
font-size: 10px;
color: var(--l2-foreground);
margin-top: 4px;
display: block;
}
}
.re-notification-container {
display: flex;
flex-direction: column;
gap: 16px;
background-color: var(--l2-background);
border: 1px solid var(--l1-border);
padding: 16px;
margin-top: 16px;
.advanced-option-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
.advanced-option-item-left-content {
display: flex;
flex-direction: column;
gap: 6px;
.advanced-option-item-title {
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
font-weight: 500;
}
.advanced-option-item-description {
color: var(--muted-foreground);
font-family: Inter;
font-size: 13px;
font-weight: 400;
}
}
}
.border-bottom {
border-bottom: 1px solid var(--l1-border);
width: 100%;
margin-left: -16px;
margin-right: -32px;
}
.re-notification-condition {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
.ant-typography {
font-size: 13px;
font-weight: 400;
color: var(--l2-foreground);
white-space: nowrap;
}
.ant-select {
width: 200px;
height: 32px;
flex-shrink: 0;
.ant-select-selector {
border: 1px solid var(--l1-border);
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
}
}
.ant-input {
width: 200px;
flex-shrink: 0;
border: 1px solid var(--l1-border);
}
}
}
}
}
.template-variable-content {
padding: 16px;
display: flex;
flex-direction: column;
gap: 2px;
.template-variable-content-item {
display: flex;
gap: 8px;
align-items: center;
code {
background-color: var(--l1-background);
color: var(--l2-foreground);
padding: 2px 4px;
}
}
}

View File

@@ -1,21 +0,0 @@
.chartPreviewContainer {
height: 100%;
width: 100%;
margin-right: var(--spacing-2);
:global(.ant-card) {
border: 1px solid var(--l1-border);
:global(.ant-card-body) {
background-color: var(--l1-background);
}
}
}
.chartPreviewHeadline {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--spacing-4);
width: 100%;
}

View File

@@ -3,8 +3,6 @@ import { useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import YAxisUnitSelector from 'components/YAxisUnitSelector';
import styles from './ChartPreview.module.scss';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryParams } from 'constants/query';
@@ -73,7 +71,7 @@ function ChartPreview({
}, [initialYAxisUnit, setAlertState, shouldUpdateYAxisUnit]);
const headline = (
<div className={styles.chartPreviewHeadline}>
<div className="chart-preview-headline">
<PlotTag
queryType={currentQuery.queryType}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
@@ -121,10 +119,7 @@ function ChartPreview({
);
return (
<div
className={styles.chartPreviewContainer}
data-testid="chart-preview-container"
>
<div className="chart-preview-container">
{currentQuery.queryType === EQueryType.QUERY_BUILDER &&
renderQBChartPreview()}
{currentQuery.queryType === EQueryType.PROM &&

View File

@@ -1,69 +0,0 @@
.querySection {
margin: 0 var(--spacing-8);
:global {
[class*='alertQuerySectionContainer'] {
margin: 0;
background-color: var(--l1-background);
border: 1px solid var(--l1-border);
}
}
}
.querySectionTabs {
display: flex;
align-items: center;
margin-left: var(--spacing-4);
margin-top: var(--spacing-12);
}
.querySectionQueryActions {
display: flex;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
flex-direction: row;
border-bottom: none;
margin-bottom: -1px;
}
.explorerViewOption {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
border: none;
padding: 9px;
box-shadow: none;
border-radius: 0;
border-left: 0.5px solid var(--l1-border);
border-bottom: 0.5px solid var(--l1-border);
width: 120px;
height: 36px;
gap: var(--spacing-4);
&:first-child {
border-left: 1px solid transparent;
}
&:hover {
background-color: transparent !important;
border-left: 1px solid transparent !important;
color: var(--l1-foreground);
}
&:disabled {
background-color: var(--l2-background);
opacity: 0.6;
}
}
.explorerViewOptionActive {
composes: explorerViewOption;
background-color: var(--l1-background);
border-bottom: none;
&:hover {
background-color: var(--l1-background) !important;
}
}

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { Button } from 'antd';
import cx from 'classnames';
import classNames from 'classnames';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -24,7 +24,8 @@ import { useCreateAlertState } from '../context';
import Stepper from '../Stepper';
import ChartPreview from './ChartPreview';
import { buildAlertDefForChartPreview } from './utils';
import styles from './QuerySection.module.scss';
import './styles.scss';
function QuerySection(): JSX.Element {
const {
@@ -129,7 +130,7 @@ function QuerySection(): JSX.Element {
];
return (
<div className={styles.querySection}>
<div className="query-section">
<Stepper stepNumber={1} label="Define the query" />
<ChartPreview
alertDef={alertDef}
@@ -137,13 +138,13 @@ function QuerySection(): JSX.Element {
isCancelled={isCancelled}
onFetchingStateChange={setIsLoadingQueries}
/>
<div className={styles.querySectionTabs}>
<div className={styles.querySectionQueryActions}>
<div className="query-section-tabs">
<div className="query-section-query-actions">
{tabs.map((tab) => (
<Button
key={tab.value}
className={cx(styles.explorerViewOption, {
[styles.explorerViewOptionActive]: alertType === tab.value,
className={classNames('list-view-tab', 'explorer-view-option', {
'active-tab': alertType === tab.value,
})}
onClick={(): void => {
setAlertType(tab.value as AlertTypes);

View File

@@ -152,7 +152,9 @@ describe('ChartPreview', () => {
it('renders the component with correct container class', () => {
renderChartPreview();
const container = screen.getByTestId('chart-preview-container');
const container = screen
.getByTestId(CHART_PREVIEW_COMPONENT_TEST_ID)
.closest('.chart-preview-container');
expect(container).toBeInTheDocument();
});

View File

@@ -144,6 +144,7 @@ const METRICS_TEXT = 'Metrics';
const QUERY_BUILDER_TEXT = 'query_builder';
const LOGS_TEXT = 'Logs';
const TRACES_TEXT = 'Traces';
const ACTIVE_TAB_CLASS = 'active-tab';
describe('QuerySection', () => {
const { useQueryBuilder } = jest.requireMock(
@@ -197,9 +198,7 @@ describe('QuerySection', () => {
renderQuerySection();
const metricsTab = screen.getByText(METRICS_TEXT).closest('button');
expect(metricsTab).toBeInTheDocument();
// The active state is reflected through CSS module classes which are dynamic
// The important thing is that the tab exists and can be interacted with
expect(metricsTab).toHaveClass(ACTIVE_TAB_CLASS);
});
it('handles alert type change when clicking on different tabs', async () => {
@@ -241,19 +240,18 @@ describe('QuerySection', () => {
const user = userEvent.setup();
renderQuerySection();
// Initially Metrics tab should be present
// Initially Metrics should be active
const metricsTab = screen.getByText(METRICS_TEXT).closest('button');
expect(metricsTab).toBeInTheDocument();
expect(metricsTab).toHaveClass(ACTIVE_TAB_CLASS);
// Click on Logs tab
const logsTab = screen.getByText(LOGS_TEXT);
await user.click(logsTab);
// Logs tab should exist and be clickable
// Logs should now be active
const logsButton = logsTab.closest('button');
expect(logsButton).toBeInTheDocument();
// CSS module classes are dynamically generated, so we verify interaction instead
expect(mockUseQueryBuilder.redirectWithQueryBuilderData).toHaveBeenCalled();
expect(logsButton).toHaveClass(ACTIVE_TAB_CLASS);
expect(metricsTab).not.toHaveClass(ACTIVE_TAB_CLASS);
});
it('passes correct props to QuerySectionComponent', () => {
@@ -272,17 +270,18 @@ describe('QuerySection', () => {
it('renders with correct container structure', () => {
renderQuerySection();
// Verify that the main elements are rendered
const metricsButton = screen.getByText(METRICS_TEXT).closest('button');
expect(metricsButton).toBeInTheDocument();
const container = screen.getByText(METRICS_TEXT).closest('.query-section');
expect(container).toBeInTheDocument();
// Check that all tabs are rendered in buttons
expect(screen.getByText(LOGS_TEXT).closest('button')).toBeInTheDocument();
expect(screen.getByText(TRACES_TEXT).closest('button')).toBeInTheDocument();
const tabsContainer = screen
.getByText(METRICS_TEXT)
.closest('.query-section-tabs');
expect(tabsContainer).toBeInTheDocument();
// Check that stepper and chart preview are present
expect(screen.getByTestId('stepper')).toBeInTheDocument();
expect(screen.getByTestId('chart-preview')).toBeInTheDocument();
const actionsContainer = screen
.getByText(METRICS_TEXT)
.closest('.query-section-query-actions');
expect(actionsContainer).toBeInTheDocument();
});
it('handles multiple rapid tab clicks correctly', async () => {
@@ -311,23 +310,18 @@ describe('QuerySection', () => {
const logsTab = screen.getByText('Logs');
await user.click(logsTab);
// Verify Logs tab is clickable and interaction happened
// Verify Logs is active
const logsButton = logsTab.closest('button');
expect(logsButton).toBeInTheDocument();
expect(
mockUseQueryBuilder.redirectWithQueryBuilderData,
).toHaveBeenCalledTimes(1);
expect(logsButton).toHaveClass(ACTIVE_TAB_CLASS);
// Click back to Metrics
const metricsTab = screen.getByText(METRICS_TEXT);
await user.click(metricsTab);
// Verify Metrics tab interaction
// Verify Metrics is active again
const metricsButton = metricsTab.closest('button');
expect(metricsButton).toBeInTheDocument();
expect(
mockUseQueryBuilder.redirectWithQueryBuilderData,
).toHaveBeenCalledTimes(2);
expect(metricsButton).toHaveClass(ACTIVE_TAB_CLASS);
expect(logsButton).not.toHaveClass(ACTIVE_TAB_CLASS);
});
it('updates the query data when the alert type changes', async () => {

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