Compare commits

..

4 Commits

Author SHA1 Message Date
Naman Verma
bb5a062ef3 Merge branch 'main' into nv/5122 2026-06-08 14:56:42 +05:30
Naman Verma
b06525bac2 Merge branch 'main' into nv/5122 2026-06-08 10:52:06 +05:30
Naman Verma
31fda2861a Merge branch 'main' into nv/5122 2026-06-05 11:12:49 +05:30
Naman Verma
d3ffefd15a perf: reuse label maps and index series by variable in formulas 2026-06-01 20:45:06 +05:30
166 changed files with 4158 additions and 4563 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

@@ -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

@@ -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 () => {

View File

@@ -0,0 +1,121 @@
.query-section {
margin: 0 16px;
.query-section-tabs {
display: flex;
align-items: center;
margin-left: 8px;
margin-top: 24px;
.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;
.prom-ql-icon {
height: 13px;
width: 13px;
}
.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);
}
}
}
.frequency-chart-view-controller {
display: flex;
align-items: center;
padding-left: 8px;
gap: 8px;
}
}
.y-axis-unit-selector-component {
margin-top: 16px;
.ant-select {
.ant-select-selector {
border: 1px solid var(--l1-border);
background: var(--l2-background);
&:hover {
border-color: var(--l1-border);
}
&:focus {
border-color: var(--l1-border);
}
}
}
}
.chart-preview-container {
margin-right: 4px;
.alert-chart-container {
.ant-card {
border: 1px solid var(--l1-border);
.ant-card-body {
background-color: var(--l1-background);
.chart-preview-headline {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
width: 100%;
.y-axis-unit-selector-component {
margin-top: 0;
}
}
}
}
}
}
.alert-query-section-container {
margin: 0;
background-color: var(--l1-background);
border: 1px solid var(--l1-border);
}
}

View File

@@ -1,4 +1,4 @@
import styles from './Stepper.module.scss';
import './styles.scss';
interface StepperProps {
stepNumber: number;
@@ -7,10 +7,10 @@ interface StepperProps {
function Stepper({ stepNumber, label }: StepperProps): JSX.Element {
return (
<div className={styles.stepperContainer}>
<div className={styles.stepNumber}>{stepNumber}</div>
<div className={styles.stepLabel}>{label}</div>
<div className={styles.dottedLine} />
<div className="stepper-container">
<div className="step-number">{stepNumber}</div>
<div className="step-label">{label}</div>
<div className="dotted-line" />
</div>
);
}

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