mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-21 17:30:32 +01:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3d13eb7ff | ||
|
|
782de2b210 | ||
|
|
d3c38693f3 | ||
|
|
8791df3697 | ||
|
|
eb719c3d0d | ||
|
|
f10435c210 | ||
|
|
f3f1e9cb59 | ||
|
|
d0370ce3ef | ||
|
|
d169761e65 | ||
|
|
87864ef5d4 | ||
|
|
2e0bc8998e | ||
|
|
7e1f4aa50d | ||
|
|
35da39247c | ||
|
|
ceccc47a34 | ||
|
|
23da5e22ec | ||
|
|
4c1b479149 | ||
|
|
f72204a8b2 | ||
|
|
deb3f385fa | ||
|
|
77ce5f86b1 | ||
|
|
ff211de441 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -231,4 +231,5 @@ cython_debug/
|
||||
# LSP config files
|
||||
pyrightconfig.json
|
||||
|
||||
|
||||
# agents
|
||||
.claude/settings.local.json
|
||||
|
||||
@@ -8,14 +8,6 @@ packages:
|
||||
filename: "alertmanager.go"
|
||||
structname: 'Mock{{.InterfaceName}}'
|
||||
pkgname: '{{.SrcPackageName}}test'
|
||||
github.com/SigNoz/signoz/pkg/types/alertmanagertypes:
|
||||
interfaces:
|
||||
MaintenanceStore:
|
||||
config:
|
||||
dir: '{{.InterfaceDir}}/alertmanagertypestest'
|
||||
filename: "maintenance.go"
|
||||
structname: 'Mock{{.InterfaceName}}'
|
||||
pkgname: '{{.SrcPackageName}}test'
|
||||
github.com/SigNoz/signoz/pkg/tokenizer:
|
||||
config:
|
||||
all: true
|
||||
|
||||
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.125.1
|
||||
image: signoz/signoz:v0.124.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.125.1
|
||||
image: signoz/signoz:v0.124.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
volumes:
|
||||
|
||||
@@ -181,7 +181,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.125.1}
|
||||
image: signoz/signoz:${VERSION:-v0.124.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.125.1}
|
||||
image: signoz/signoz:${VERSION:-v0.124.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -96,53 +96,6 @@ components:
|
||||
- createdAt
|
||||
- updatedAt
|
||||
type: object
|
||||
AlertmanagertypesMaintenanceKind:
|
||||
enum:
|
||||
- fixed
|
||||
- recurring
|
||||
type: string
|
||||
AlertmanagertypesMaintenanceStatus:
|
||||
enum:
|
||||
- active
|
||||
- upcoming
|
||||
- expired
|
||||
type: string
|
||||
AlertmanagertypesPlannedMaintenance:
|
||||
properties:
|
||||
alertIds:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
kind:
|
||||
$ref: '#/components/schemas/AlertmanagertypesMaintenanceKind'
|
||||
name:
|
||||
type: string
|
||||
schedule:
|
||||
$ref: '#/components/schemas/AlertmanagertypesSchedule'
|
||||
status:
|
||||
$ref: '#/components/schemas/AlertmanagertypesMaintenanceStatus'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- schedule
|
||||
- status
|
||||
- kind
|
||||
type: object
|
||||
AlertmanagertypesPostableChannel:
|
||||
oneOf:
|
||||
- required:
|
||||
@@ -259,23 +212,6 @@ components:
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
AlertmanagertypesPostablePlannedMaintenance:
|
||||
properties:
|
||||
alertIds:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
schedule:
|
||||
$ref: '#/components/schemas/AlertmanagertypesSchedule'
|
||||
required:
|
||||
- name
|
||||
- schedule
|
||||
type: object
|
||||
AlertmanagertypesPostableRoutePolicy:
|
||||
properties:
|
||||
channels:
|
||||
@@ -301,60 +237,6 @@ components:
|
||||
- channels
|
||||
- name
|
||||
type: object
|
||||
AlertmanagertypesRecurrence:
|
||||
properties:
|
||||
duration:
|
||||
type: string
|
||||
endTime:
|
||||
format: date-time
|
||||
nullable: true
|
||||
type: string
|
||||
repeatOn:
|
||||
items:
|
||||
$ref: '#/components/schemas/AlertmanagertypesRepeatOn'
|
||||
nullable: true
|
||||
type: array
|
||||
repeatType:
|
||||
$ref: '#/components/schemas/AlertmanagertypesRepeatType'
|
||||
startTime:
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- startTime
|
||||
- duration
|
||||
- repeatType
|
||||
type: object
|
||||
AlertmanagertypesRepeatOn:
|
||||
enum:
|
||||
- sunday
|
||||
- monday
|
||||
- tuesday
|
||||
- wednesday
|
||||
- thursday
|
||||
- friday
|
||||
- saturday
|
||||
type: string
|
||||
AlertmanagertypesRepeatType:
|
||||
enum:
|
||||
- daily
|
||||
- weekly
|
||||
- monthly
|
||||
type: string
|
||||
AlertmanagertypesSchedule:
|
||||
properties:
|
||||
endTime:
|
||||
format: date-time
|
||||
type: string
|
||||
recurrence:
|
||||
$ref: '#/components/schemas/AlertmanagertypesRecurrence'
|
||||
startTime:
|
||||
format: date-time
|
||||
type: string
|
||||
timezone:
|
||||
type: string
|
||||
required:
|
||||
- timezone
|
||||
type: object
|
||||
AuthtypesAttributeMapping:
|
||||
properties:
|
||||
email:
|
||||
@@ -2342,8 +2224,6 @@ components:
|
||||
type: boolean
|
||||
org_id:
|
||||
type: string
|
||||
source:
|
||||
$ref: '#/components/schemas/DashboardtypesSource'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
@@ -2373,12 +2253,6 @@ components:
|
||||
timeRangeEnabled:
|
||||
type: boolean
|
||||
type: object
|
||||
DashboardtypesSource:
|
||||
enum:
|
||||
- user
|
||||
- system
|
||||
- integration
|
||||
type: object
|
||||
DashboardtypesStorableDashboardData:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
@@ -5263,6 +5137,17 @@ components:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
RuletypesMaintenanceKind:
|
||||
enum:
|
||||
- fixed
|
||||
- recurring
|
||||
type: string
|
||||
RuletypesMaintenanceStatus:
|
||||
enum:
|
||||
- active
|
||||
- upcoming
|
||||
- expired
|
||||
type: string
|
||||
RuletypesMatchType:
|
||||
enum:
|
||||
- at_least_once
|
||||
@@ -5290,6 +5175,59 @@ components:
|
||||
- table
|
||||
- graph
|
||||
type: string
|
||||
RuletypesPlannedMaintenance:
|
||||
properties:
|
||||
alertIds:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
kind:
|
||||
$ref: '#/components/schemas/RuletypesMaintenanceKind'
|
||||
name:
|
||||
type: string
|
||||
schedule:
|
||||
$ref: '#/components/schemas/RuletypesSchedule'
|
||||
status:
|
||||
$ref: '#/components/schemas/RuletypesMaintenanceStatus'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- schedule
|
||||
- status
|
||||
- kind
|
||||
type: object
|
||||
RuletypesPostablePlannedMaintenance:
|
||||
properties:
|
||||
alertIds:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
schedule:
|
||||
$ref: '#/components/schemas/RuletypesSchedule'
|
||||
required:
|
||||
- name
|
||||
- schedule
|
||||
type: object
|
||||
RuletypesPostableRule:
|
||||
properties:
|
||||
alert:
|
||||
@@ -5342,6 +5280,29 @@ components:
|
||||
- clickhouse_sql
|
||||
- promql
|
||||
type: string
|
||||
RuletypesRecurrence:
|
||||
properties:
|
||||
duration:
|
||||
type: string
|
||||
endTime:
|
||||
format: date-time
|
||||
nullable: true
|
||||
type: string
|
||||
repeatOn:
|
||||
items:
|
||||
$ref: '#/components/schemas/RuletypesRepeatOn'
|
||||
nullable: true
|
||||
type: array
|
||||
repeatType:
|
||||
$ref: '#/components/schemas/RuletypesRepeatType'
|
||||
startTime:
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- startTime
|
||||
- duration
|
||||
- repeatType
|
||||
type: object
|
||||
RuletypesRenotify:
|
||||
properties:
|
||||
alertStates:
|
||||
@@ -5353,6 +5314,22 @@ components:
|
||||
interval:
|
||||
type: string
|
||||
type: object
|
||||
RuletypesRepeatOn:
|
||||
enum:
|
||||
- sunday
|
||||
- monday
|
||||
- tuesday
|
||||
- wednesday
|
||||
- thursday
|
||||
- friday
|
||||
- saturday
|
||||
type: string
|
||||
RuletypesRepeatType:
|
||||
enum:
|
||||
- daily
|
||||
- weekly
|
||||
- monthly
|
||||
type: string
|
||||
RuletypesRollingWindow:
|
||||
properties:
|
||||
evalWindow:
|
||||
@@ -5472,6 +5449,21 @@ components:
|
||||
- promql_rule
|
||||
- anomaly_rule
|
||||
type: string
|
||||
RuletypesSchedule:
|
||||
properties:
|
||||
endTime:
|
||||
format: date-time
|
||||
type: string
|
||||
recurrence:
|
||||
$ref: '#/components/schemas/RuletypesRecurrence'
|
||||
startTime:
|
||||
format: date-time
|
||||
type: string
|
||||
timezone:
|
||||
type: string
|
||||
required:
|
||||
- timezone
|
||||
type: object
|
||||
RuletypesScheduleType:
|
||||
enum:
|
||||
- hourly
|
||||
@@ -8032,7 +8024,7 @@ paths:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
|
||||
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
@@ -8075,7 +8067,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlertmanagertypesPostablePlannedMaintenance'
|
||||
$ref: '#/components/schemas/RuletypesPostablePlannedMaintenance'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
@@ -8083,7 +8075,7 @@ paths:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
|
||||
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
@@ -8186,7 +8178,7 @@ paths:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
|
||||
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
@@ -8240,7 +8232,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlertmanagertypesPostablePlannedMaintenance'
|
||||
$ref: '#/components/schemas/RuletypesPostablePlannedMaintenance'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
|
||||
@@ -49,14 +49,6 @@ func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publi
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dashboard.ErrIfNotPublishable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storablePublicDashboard, err := module.store.GetPublic(ctx, publicDashboard.DashboardID.StringValue())
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
@@ -137,14 +129,6 @@ func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publi
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dashboard.ErrIfNotPublishable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.UpdatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(publicDashboard))
|
||||
}
|
||||
|
||||
@@ -154,10 +138,6 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
|
||||
return err
|
||||
}
|
||||
|
||||
if err := dashboard.ErrIfNotDeletable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dashboard.Locked {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
|
||||
}
|
||||
@@ -188,14 +168,6 @@ func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashb
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
dashboard, err := module.Get(ctx, orgID, dashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dashboard.ErrIfNotPublishable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.store.DeletePublic(ctx, dashboardID.StringValue())
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) {
|
||||
@@ -48,7 +49,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
rules = append(rules, tr)
|
||||
|
||||
// create ch rule task for evaluation
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
|
||||
|
||||
@@ -72,7 +73,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
rules = append(rules, pr)
|
||||
|
||||
// create promql rule task for evaluation
|
||||
task = newTask(baserules.TaskTypeProm, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
|
||||
task = newTask(baserules.TaskTypeProm, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeAnomaly {
|
||||
// create anomaly rule
|
||||
@@ -95,7 +96,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
rules = append(rules, ar)
|
||||
|
||||
// create anomaly rule task for evaluation
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
|
||||
@@ -209,9 +210,9 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
|
||||
}
|
||||
|
||||
// newTask returns an appropriate group for the rule type
|
||||
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc) baserules.Task {
|
||||
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, maintenanceStore ruletypes.MaintenanceStore, orgID valuer.UUID) baserules.Task {
|
||||
if taskType == baserules.TaskTypeCh {
|
||||
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify)
|
||||
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
|
||||
}
|
||||
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify)
|
||||
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export default defineConfig({
|
||||
signal: true,
|
||||
useOperationIdAsQueryKey: false,
|
||||
},
|
||||
useDates: false,
|
||||
useDates: true,
|
||||
useNamedParameters: true,
|
||||
enumGenerationType: 'enum',
|
||||
mutator: {
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
const BANNED_COMPONENTS = {
|
||||
Typography: 'Use @signozhq/ui Typography instead of antd Typography.',
|
||||
Badge: 'Use @signozhq/ui/badge instead of antd Badge.',
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -47,6 +47,7 @@ export const TracesFunnels = Loadable(
|
||||
import(/* webpackChunkName: "Traces Funnels" */ 'pages/TracesModulePage'),
|
||||
);
|
||||
export const TracesFunnelDetails = Loadable(
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "Traces Funnel Details" */ 'pages/TracesModulePage'
|
||||
@@ -312,6 +313,13 @@ export const PublicDashboardPage = Loadable(
|
||||
),
|
||||
);
|
||||
|
||||
export const AlertTypeSelectionPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "Alert Type Selection Page" */ 'pages/AlertTypeSelection'
|
||||
),
|
||||
);
|
||||
|
||||
export const MeterExplorerPage = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "Meter Explorer Page" */ 'pages/MeterExplorer'),
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
AIAssistantPage,
|
||||
AlertHistory,
|
||||
AlertOverview,
|
||||
AlertTypeSelectionPage,
|
||||
AllAlertChannels,
|
||||
AllErrors,
|
||||
ApiMonitoring,
|
||||
@@ -212,6 +213,13 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'LIST_ALL_ALERT',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ALERT_TYPE_SELECTION,
|
||||
exact: true,
|
||||
component: AlertTypeSelectionPage,
|
||||
isPrivate: true,
|
||||
key: 'ALERT_TYPE_SELECTION',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ALERTS_NEW,
|
||||
exact: true,
|
||||
@@ -525,6 +533,18 @@ export const LIST_LICENSES: AppRoutes = {
|
||||
key: 'LIST_LICENSES',
|
||||
};
|
||||
|
||||
export const oldRoutes = [
|
||||
'/pipelines',
|
||||
'/logs-explorer',
|
||||
'/logs-explorer/live',
|
||||
'/logs-save-views',
|
||||
'/traces-save-views',
|
||||
'/settings/access-tokens',
|
||||
'/settings/api-keys',
|
||||
'/messaging-queues',
|
||||
'/alerts/edit',
|
||||
];
|
||||
|
||||
export const oldNewRoutesMapping: Record<string, string> = {
|
||||
'/pipelines': '/logs/pipelines',
|
||||
'/logs-explorer': '/logs/logs-explorer',
|
||||
@@ -535,9 +555,7 @@ export const oldNewRoutesMapping: Record<string, string> = {
|
||||
'/settings/api-keys': '/settings/service-accounts',
|
||||
'/messaging-queues': '/messaging-queues/overview',
|
||||
'/alerts/edit': '/alerts/overview',
|
||||
'/alerts/type-selection': '/alerts/new',
|
||||
};
|
||||
export const oldRoutes = Object.keys(oldNewRoutesMapping);
|
||||
|
||||
export const ROUTES_NOT_TO_BE_OVERRIDEN: string[] = [
|
||||
ROUTES.WORKSPACE_LOCKED,
|
||||
|
||||
@@ -18,7 +18,6 @@ import type {
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
AlertmanagertypesPostablePlannedMaintenanceDTO,
|
||||
CreateDowntimeSchedule201,
|
||||
DeleteDowntimeScheduleByIDPathParameters,
|
||||
GetDowntimeScheduleByID200,
|
||||
@@ -26,6 +25,7 @@ import type {
|
||||
ListDowntimeSchedules200,
|
||||
ListDowntimeSchedulesParams,
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesPostablePlannedMaintenanceDTO,
|
||||
UpdateDowntimeScheduleByIDPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
@@ -135,14 +135,14 @@ export const invalidateListDowntimeSchedules = async (
|
||||
* @summary Create downtime schedule
|
||||
*/
|
||||
export const createDowntimeSchedule = (
|
||||
alertmanagertypesPostablePlannedMaintenanceDTO?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>,
|
||||
ruletypesPostablePlannedMaintenanceDTO?: BodyType<RuletypesPostablePlannedMaintenanceDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateDowntimeSchedule201>({
|
||||
url: `/api/v1/downtime_schedules`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: alertmanagertypesPostablePlannedMaintenanceDTO,
|
||||
data: ruletypesPostablePlannedMaintenanceDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
@@ -154,13 +154,13 @@ export const getCreateDowntimeScheduleMutationOptions = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
|
||||
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
|
||||
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createDowntimeSchedule'];
|
||||
@@ -174,7 +174,7 @@ export const getCreateDowntimeScheduleMutationOptions = <
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> }
|
||||
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
@@ -188,7 +188,7 @@ export type CreateDowntimeScheduleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>
|
||||
>;
|
||||
export type CreateDowntimeScheduleMutationBody =
|
||||
| BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>
|
||||
| BodyType<RuletypesPostablePlannedMaintenanceDTO>
|
||||
| undefined;
|
||||
export type CreateDowntimeScheduleMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
@@ -203,13 +203,13 @@ export const useCreateDowntimeSchedule = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
|
||||
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
|
||||
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateDowntimeScheduleMutationOptions(options));
|
||||
@@ -403,14 +403,14 @@ export const invalidateGetDowntimeScheduleByID = async (
|
||||
*/
|
||||
export const updateDowntimeScheduleByID = (
|
||||
{ id }: UpdateDowntimeScheduleByIDPathParameters,
|
||||
alertmanagertypesPostablePlannedMaintenanceDTO?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>,
|
||||
ruletypesPostablePlannedMaintenanceDTO?: BodyType<RuletypesPostablePlannedMaintenanceDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/downtime_schedules/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: alertmanagertypesPostablePlannedMaintenanceDTO,
|
||||
data: ruletypesPostablePlannedMaintenanceDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
@@ -424,7 +424,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
|
||||
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -433,7 +433,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
|
||||
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
@@ -450,7 +450,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
|
||||
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
@@ -465,7 +465,7 @@ export type UpdateDowntimeScheduleByIDMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>
|
||||
>;
|
||||
export type UpdateDowntimeScheduleByIDMutationBody =
|
||||
| BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>
|
||||
| BodyType<RuletypesPostablePlannedMaintenanceDTO>
|
||||
| undefined;
|
||||
export type UpdateDowntimeScheduleByIDMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
@@ -482,7 +482,7 @@ export const useUpdateDowntimeScheduleByID = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
|
||||
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -491,7 +491,7 @@ export const useUpdateDowntimeScheduleByID = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
|
||||
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface AlertmanagertypesChannelDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -34,7 +34,7 @@ export interface AlertmanagertypesChannelDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface ModelLabelSetDTO {
|
||||
@@ -62,7 +62,7 @@ export interface AlertmanagertypesDeprecatedGettableAlertDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
endsAt?: string;
|
||||
endsAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -80,7 +80,7 @@ export interface AlertmanagertypesDeprecatedGettableAlertDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
startsAt?: string;
|
||||
startsAt?: Date;
|
||||
status?: TypesAlertStatusDTO;
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export interface AlertmanagertypesGettableRoutePolicyDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt: string;
|
||||
createdAt: Date;
|
||||
/**
|
||||
* @type string,null
|
||||
*/
|
||||
@@ -127,116 +127,13 @@ export interface AlertmanagertypesGettableRoutePolicyDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt: string;
|
||||
updatedAt: Date;
|
||||
/**
|
||||
* @type string,null
|
||||
*/
|
||||
updatedBy?: string | null;
|
||||
}
|
||||
|
||||
export enum AlertmanagertypesMaintenanceKindDTO {
|
||||
fixed = 'fixed',
|
||||
recurring = 'recurring',
|
||||
}
|
||||
export enum AlertmanagertypesMaintenanceStatusDTO {
|
||||
active = 'active',
|
||||
upcoming = 'upcoming',
|
||||
expired = 'expired',
|
||||
}
|
||||
export enum AlertmanagertypesRepeatOnDTO {
|
||||
sunday = 'sunday',
|
||||
monday = 'monday',
|
||||
tuesday = 'tuesday',
|
||||
wednesday = 'wednesday',
|
||||
thursday = 'thursday',
|
||||
friday = 'friday',
|
||||
saturday = 'saturday',
|
||||
}
|
||||
export enum AlertmanagertypesRepeatTypeDTO {
|
||||
daily = 'daily',
|
||||
weekly = 'weekly',
|
||||
monthly = 'monthly',
|
||||
}
|
||||
export interface AlertmanagertypesRecurrenceDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
duration: string;
|
||||
/**
|
||||
* @type string,null
|
||||
* @format date-time
|
||||
*/
|
||||
endTime?: string | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
repeatOn?: AlertmanagertypesRepeatOnDTO[] | null;
|
||||
repeatType: AlertmanagertypesRepeatTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
startTime: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesScheduleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
endTime?: string;
|
||||
recurrence?: AlertmanagertypesRecurrenceDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
startTime?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesPlannedMaintenanceDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
alertIds?: string[] | null;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
kind: AlertmanagertypesMaintenanceKindDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
schedule: AlertmanagertypesScheduleDTO;
|
||||
status: AlertmanagertypesMaintenanceStatusDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface ConfigAuthorizationDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -1700,22 +1597,6 @@ export type AlertmanagertypesPostableChannelDTO = unknown & {
|
||||
wechat_configs?: ConfigWechatConfigDTO[];
|
||||
};
|
||||
|
||||
export interface AlertmanagertypesPostablePlannedMaintenanceDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
alertIds?: string[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
schedule: AlertmanagertypesScheduleDTO;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesPostableRoutePolicyDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
@@ -1953,7 +1834,7 @@ export interface AuthtypesGettableAuthDomainDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -1970,7 +1851,7 @@ export interface AuthtypesGettableAuthDomainDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface AuthtypesGettableTokenDTO {
|
||||
@@ -2128,7 +2009,7 @@ export interface AuthtypesRoleDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2153,7 +2034,7 @@ export interface AuthtypesRoleDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface AuthtypesSessionContextDTO {
|
||||
@@ -2181,7 +2062,7 @@ export interface AuthtypesUserRoleDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt: string;
|
||||
createdAt: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2195,7 +2076,7 @@ export interface AuthtypesUserRoleDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt: string;
|
||||
updatedAt: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2207,7 +2088,7 @@ export interface AuthtypesUserWithRolesDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2236,7 +2117,7 @@ export interface AuthtypesUserWithRolesDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -2403,7 +2284,7 @@ export interface CloudintegrationtypesAccountDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2424,12 +2305,12 @@ export interface CloudintegrationtypesAccountDTO {
|
||||
* @type string,null
|
||||
* @format date-time
|
||||
*/
|
||||
removedAt: string | null;
|
||||
removedAt: Date | null;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface DashboardtypesStorableDashboardDataDTO {
|
||||
@@ -2560,7 +2441,7 @@ export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2570,7 +2451,7 @@ export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2764,12 +2645,12 @@ export interface CloudintegrationtypesGettableAgentCheckInDTO {
|
||||
* @type string,null
|
||||
* @format date-time
|
||||
*/
|
||||
removed_at: string | null;
|
||||
removed_at: Date | null;
|
||||
/**
|
||||
* @type string,null
|
||||
* @format date-time
|
||||
*/
|
||||
removedAt: string | null;
|
||||
removedAt: Date | null;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesServiceMetadataDTO {
|
||||
@@ -2999,17 +2880,12 @@ export interface CoretypesPatchableObjectsDTO {
|
||||
deletions: CoretypesObjectGroupDTO[] | null;
|
||||
}
|
||||
|
||||
export enum DashboardtypesSourceDTO {
|
||||
user = 'user',
|
||||
system = 'system',
|
||||
integration = 'integration',
|
||||
}
|
||||
export interface DashboardtypesDashboardDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -3027,12 +2903,11 @@ export interface DashboardtypesDashboardDTO {
|
||||
* @type string
|
||||
*/
|
||||
org_id?: string;
|
||||
source?: DashboardtypesSourceDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -3214,7 +3089,7 @@ export interface GatewaytypesLimitDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
created_at?: string;
|
||||
created_at?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -3236,7 +3111,7 @@ export interface GatewaytypesLimitDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updated_at?: string;
|
||||
updated_at?: Date;
|
||||
}
|
||||
|
||||
export interface GatewaytypesIngestionKeyDTO {
|
||||
@@ -3244,12 +3119,12 @@ export interface GatewaytypesIngestionKeyDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
created_at?: string;
|
||||
created_at?: Date;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
expires_at?: string;
|
||||
expires_at?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -3270,7 +3145,7 @@ export interface GatewaytypesIngestionKeyDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updated_at?: string;
|
||||
updated_at?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -3294,7 +3169,7 @@ export interface GatewaytypesPostableIngestionKeyDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
expires_at?: string;
|
||||
expires_at?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -4564,7 +4439,7 @@ export interface LlmpricingruletypesLLMPricingRuleDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -4603,13 +4478,13 @@ export interface LlmpricingruletypesLLMPricingRuleDTO {
|
||||
* @type string,null
|
||||
* @format date-time
|
||||
*/
|
||||
syncedAt?: string | null;
|
||||
syncedAt?: Date | null;
|
||||
unit: LlmpricingruletypesLLMPricingRuleUnitDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -5835,7 +5710,7 @@ export interface Querybuildertypesv5RawRowDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
timestamp?: string;
|
||||
timestamp?: Date;
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5RawDataDTO {
|
||||
@@ -6241,6 +6116,15 @@ export interface RuletypesGettableTestRuleDTO {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export enum RuletypesMaintenanceKindDTO {
|
||||
fixed = 'fixed',
|
||||
recurring = 'recurring',
|
||||
}
|
||||
export enum RuletypesMaintenanceStatusDTO {
|
||||
active = 'active',
|
||||
upcoming = 'upcoming',
|
||||
expired = 'expired',
|
||||
}
|
||||
export interface RuletypesRenotifyDTO {
|
||||
/**
|
||||
* @type array
|
||||
@@ -6272,6 +6156,116 @@ export interface RuletypesNotificationSettingsDTO {
|
||||
usePolicy?: boolean;
|
||||
}
|
||||
|
||||
export enum RuletypesRepeatOnDTO {
|
||||
sunday = 'sunday',
|
||||
monday = 'monday',
|
||||
tuesday = 'tuesday',
|
||||
wednesday = 'wednesday',
|
||||
thursday = 'thursday',
|
||||
friday = 'friday',
|
||||
saturday = 'saturday',
|
||||
}
|
||||
export enum RuletypesRepeatTypeDTO {
|
||||
daily = 'daily',
|
||||
weekly = 'weekly',
|
||||
monthly = 'monthly',
|
||||
}
|
||||
export interface RuletypesRecurrenceDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
duration: string;
|
||||
/**
|
||||
* @type string,null
|
||||
* @format date-time
|
||||
*/
|
||||
endTime?: Date | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
repeatOn?: RuletypesRepeatOnDTO[] | null;
|
||||
repeatType: RuletypesRepeatTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
startTime: Date;
|
||||
}
|
||||
|
||||
export interface RuletypesScheduleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
endTime?: Date;
|
||||
recurrence?: RuletypesRecurrenceDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
startTime?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export interface RuletypesPlannedMaintenanceDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
alertIds?: string[] | null;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
kind: RuletypesMaintenanceKindDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
schedule: RuletypesScheduleDTO;
|
||||
status: RuletypesMaintenanceStatusDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface RuletypesPostablePlannedMaintenanceDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
alertIds?: string[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
schedule: RuletypesScheduleDTO;
|
||||
}
|
||||
|
||||
export type RuletypesPostableRuleDTOAnnotations = { [key: string]: string };
|
||||
|
||||
export type RuletypesPostableRuleDTOLabels = { [key: string]: string };
|
||||
@@ -6412,7 +6406,7 @@ export interface RuletypesRuleDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6461,7 +6455,7 @@ export interface RuletypesRuleDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6480,7 +6474,7 @@ export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
@@ -6494,7 +6488,7 @@ export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
lastObservedAt: string;
|
||||
lastObservedAt: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6507,7 +6501,7 @@ export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesGettableFactorAPIKeyWithKeyDTO {
|
||||
@@ -6552,7 +6546,7 @@ export interface ServiceaccounttypesServiceAccountDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6577,7 +6571,7 @@ export interface ServiceaccounttypesServiceAccountDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesServiceAccountRoleDTO {
|
||||
@@ -6585,7 +6579,7 @@ export interface ServiceaccounttypesServiceAccountRoleDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6603,7 +6597,7 @@ export interface ServiceaccounttypesServiceAccountRoleDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesServiceAccountWithRolesDTO {
|
||||
@@ -6611,7 +6605,7 @@ export interface ServiceaccounttypesServiceAccountWithRolesDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6640,7 +6634,7 @@ export interface ServiceaccounttypesServiceAccountWithRolesDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesUpdatableFactorAPIKeyDTO {
|
||||
@@ -6682,7 +6676,7 @@ export interface SpantypesSpanMapperGroupDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6707,7 +6701,7 @@ export interface SpantypesSpanMapperGroupDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6776,7 +6770,7 @@ export interface SpantypesSpanMapperDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6802,7 +6796,7 @@ export interface SpantypesSpanMapperDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -7169,7 +7163,7 @@ export interface TypesDeprecatedUserDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -7202,7 +7196,7 @@ export interface TypesDeprecatedUserDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface TypesIdentifiableDTO {
|
||||
@@ -7217,7 +7211,7 @@ export interface TypesInviteDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -7250,7 +7244,7 @@ export interface TypesInviteDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface TypesOrganizationDTO {
|
||||
@@ -7262,7 +7256,7 @@ export interface TypesOrganizationDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -7284,7 +7278,7 @@ export interface TypesOrganizationDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface TypesPostableInviteDTO {
|
||||
@@ -7351,7 +7345,7 @@ export interface TypesResetPasswordTokenDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
expiresAt?: string;
|
||||
expiresAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -7378,7 +7372,7 @@ export interface TypesUserDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -7407,7 +7401,7 @@ export interface TypesUserDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface ZeustypesHostDTO {
|
||||
@@ -7799,7 +7793,7 @@ export type ListDowntimeSchedules200 = {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
data: AlertmanagertypesPlannedMaintenanceDTO[];
|
||||
data: RuletypesPlannedMaintenanceDTO[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -7807,7 +7801,7 @@ export type ListDowntimeSchedules200 = {
|
||||
};
|
||||
|
||||
export type CreateDowntimeSchedule201 = {
|
||||
data: AlertmanagertypesPlannedMaintenanceDTO;
|
||||
data: RuletypesPlannedMaintenanceDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -7821,7 +7815,7 @@ export type GetDowntimeScheduleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetDowntimeScheduleByID200 = {
|
||||
data: AlertmanagertypesPlannedMaintenanceDTO;
|
||||
data: RuletypesPlannedMaintenanceDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
.breadcrumb {
|
||||
padding-left: 16px;
|
||||
|
||||
ol {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global(.ant-breadcrumb-separator) {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-color: var(--l1-border);
|
||||
margin: 16px 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Breadcrumb, Divider } from 'antd';
|
||||
|
||||
import styles from './AlertBreadcrumb.module.scss';
|
||||
import BreadcrumbItem, { BreadcrumbItemConfig } from './BreadcrumbItem';
|
||||
|
||||
export interface AlertBreadcrumbProps {
|
||||
items: BreadcrumbItemConfig[];
|
||||
className?: string;
|
||||
showDivider?: boolean;
|
||||
}
|
||||
|
||||
function AlertBreadcrumb({
|
||||
items,
|
||||
className,
|
||||
showDivider = true,
|
||||
}: AlertBreadcrumbProps): JSX.Element {
|
||||
const breadcrumbItems = items.map((item) => ({
|
||||
title: <BreadcrumbItem {...item} />,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb
|
||||
className={`${styles.breadcrumb} ${className || ''}`}
|
||||
items={breadcrumbItems}
|
||||
/>
|
||||
{showDivider && <Divider className={styles.divider} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AlertBreadcrumb;
|
||||
@@ -1,9 +0,0 @@
|
||||
.item {
|
||||
--button-padding: 0;
|
||||
--button-font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
|
||||
.itemLast {
|
||||
color: var(--muted-foreground);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import styles from './BreadcrumbItem.module.scss';
|
||||
|
||||
export type BreadcrumbItemConfig =
|
||||
| {
|
||||
title: string | null;
|
||||
route?: string;
|
||||
}
|
||||
| {
|
||||
title: string | null;
|
||||
isLast?: true;
|
||||
};
|
||||
|
||||
function BreadcrumbItem({
|
||||
title,
|
||||
...props
|
||||
}: BreadcrumbItemConfig): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
if ('isLast' in props) {
|
||||
return <div className={styles.itemLast}>{title}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className={styles.item}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
if (!('route' in props) || !props.route) {
|
||||
return;
|
||||
}
|
||||
|
||||
safeNavigate(props.route, { newTab: isModifierKeyPressed(e) });
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default BreadcrumbItem;
|
||||
@@ -1,6 +0,0 @@
|
||||
export { default } from './AlertBreadcrumb';
|
||||
export {
|
||||
default as BreadcrumbItem,
|
||||
type BreadcrumbItemConfig,
|
||||
} from './BreadcrumbItem';
|
||||
export type { AlertBreadcrumbProps } from './AlertBreadcrumb';
|
||||
@@ -59,7 +59,7 @@ function getDeleteTooltip(
|
||||
|
||||
function getInviteButtonLabel(
|
||||
isLoading: boolean,
|
||||
existingToken: { expiresAt?: string } | undefined,
|
||||
existingToken: { expiresAt?: Date } | undefined,
|
||||
isExpired: boolean,
|
||||
notFound: boolean,
|
||||
): string {
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
|
||||
import { FloatingPanel } from 'periscope/components/FloatingPanel';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import AddedFields from './AddedFields';
|
||||
import OtherFields from './OtherFields';
|
||||
|
||||
import styles from './FieldsSelector.module.scss';
|
||||
|
||||
const DEFAULT_PANEL_WIDTH = 350;
|
||||
const DEFAULT_PANEL_HEIGHT_OFFSET = 100;
|
||||
const DEFAULT_PANEL_RIGHT_INSET = 100;
|
||||
const DEFAULT_PANEL_TOP_INSET = 50;
|
||||
|
||||
interface FieldsSelectorProps {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
fields: TelemetryFieldKey[];
|
||||
onFieldsChange: (fields: TelemetryFieldKey[]) => void;
|
||||
onClose: () => void;
|
||||
signal: DataSource;
|
||||
maxFields?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
defaultPosition?: { x: number; y: number };
|
||||
}
|
||||
|
||||
function FieldsSelector({
|
||||
isOpen,
|
||||
title,
|
||||
fields,
|
||||
onFieldsChange,
|
||||
onClose,
|
||||
signal,
|
||||
maxFields,
|
||||
width = DEFAULT_PANEL_WIDTH,
|
||||
height,
|
||||
defaultPosition,
|
||||
}: FieldsSelectorProps): JSX.Element | null {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolvedHeight =
|
||||
height ?? window.innerHeight - DEFAULT_PANEL_HEIGHT_OFFSET;
|
||||
const resolvedPosition = defaultPosition ?? {
|
||||
x: window.innerWidth - width - DEFAULT_PANEL_RIGHT_INSET,
|
||||
y: DEFAULT_PANEL_TOP_INSET,
|
||||
};
|
||||
const [draftFields, setDraftFields] = useState<TelemetryFieldKey[]>(fields);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [debouncedInputValue, setDebouncedInputValue] = useState('');
|
||||
|
||||
const debouncedUpdate = useDebouncedFn((value) => {
|
||||
setDebouncedInputValue(value as string);
|
||||
}, 400);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const value = e.target.value.trim().toLowerCase();
|
||||
setInputValue(value);
|
||||
debouncedUpdate(value);
|
||||
},
|
||||
[debouncedUpdate],
|
||||
);
|
||||
|
||||
const handleAdd = useCallback(
|
||||
(field: TelemetryFieldKey): void => {
|
||||
if (maxFields !== undefined && draftFields.length >= maxFields) {
|
||||
return;
|
||||
}
|
||||
if (draftFields.some((f) => f.name === field.name)) {
|
||||
return;
|
||||
}
|
||||
setDraftFields((prev) => [...prev, field]);
|
||||
},
|
||||
[draftFields, maxFields],
|
||||
);
|
||||
|
||||
const handleSave = useCallback((): void => {
|
||||
onFieldsChange(draftFields);
|
||||
toast.success('Saved successfully', {
|
||||
position: 'top-right',
|
||||
});
|
||||
onClose();
|
||||
}, [draftFields, onFieldsChange, onClose]);
|
||||
|
||||
const handleDiscard = useCallback((): void => {
|
||||
setDraftFields(fields);
|
||||
}, [fields]);
|
||||
|
||||
const hasUnsavedChanges = useMemo(
|
||||
() =>
|
||||
!(
|
||||
draftFields.length === fields.length &&
|
||||
draftFields.every((f, i) => f.name === fields[i]?.name)
|
||||
),
|
||||
[draftFields, fields],
|
||||
);
|
||||
|
||||
const isAtLimit = maxFields !== undefined && draftFields.length >= maxFields;
|
||||
|
||||
return (
|
||||
<FloatingPanel
|
||||
isOpen
|
||||
width={width}
|
||||
height={resolvedHeight}
|
||||
defaultPosition={resolvedPosition}
|
||||
enableResizing={false}
|
||||
>
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>
|
||||
<TableColumnsSplit size={16} />
|
||||
{title}
|
||||
</div>
|
||||
<X className={styles.closeIcon} size={16} onClick={onClose} />
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<Input
|
||||
className={styles.searchInput}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
placeholder="Search for a field..."
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<AddedFields
|
||||
inputValue={inputValue}
|
||||
fields={draftFields}
|
||||
onFieldsChange={setDraftFields}
|
||||
maxFields={maxFields}
|
||||
/>
|
||||
|
||||
<OtherFields
|
||||
signal={signal}
|
||||
debouncedInputValue={debouncedInputValue}
|
||||
addedFields={draftFields}
|
||||
onAdd={handleAdd}
|
||||
isAtLimit={isAtLimit}
|
||||
/>
|
||||
|
||||
{hasUnsavedChanges && (
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={handleDiscard}
|
||||
prefix={<X width={14} height={14} />}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={handleSave}
|
||||
prefix={<Check width={14} height={14} />}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</FloatingPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default FieldsSelector;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './FieldsSelector';
|
||||
@@ -5,8 +5,6 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Popover } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { AIAssistantEvents } from 'container/AIAssistant/events';
|
||||
import { normalizePage } from 'container/AIAssistant/hooks/useAIAssistantAnalyticsContext';
|
||||
import {
|
||||
openAIAssistant,
|
||||
useAIAssistantStore,
|
||||
@@ -52,14 +50,6 @@ function HeaderRightSection({
|
||||
setOpenAnnouncementsModal(false);
|
||||
}, [location.pathname]);
|
||||
|
||||
const handleOpenAIAssistant = useCallback((): void => {
|
||||
void logEvent(AIAssistantEvents.Opened, {
|
||||
source: 'header',
|
||||
currentPage: normalizePage(location.pathname),
|
||||
});
|
||||
openAIAssistant();
|
||||
}, [location.pathname]);
|
||||
|
||||
const handleOpenShareURLModal = useCallback((): void => {
|
||||
logEvent('Share: Clicked', {
|
||||
page: location.pathname,
|
||||
@@ -111,7 +101,7 @@ function HeaderRightSection({
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onClick={handleOpenAIAssistant}
|
||||
onClick={openAIAssistant}
|
||||
aria-label={
|
||||
showHeaderPendingBadge
|
||||
? pendingUserInputCount === 1
|
||||
|
||||
@@ -50,7 +50,6 @@ import {
|
||||
import { JsonView } from 'periscope/components/JsonView';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILogBody } from 'types/api/logs/log';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@@ -218,17 +217,20 @@ function LogDetailInner({
|
||||
|
||||
const logBody = useMemo(() => {
|
||||
if (!isBodyJsonQueryEnabled) {
|
||||
return (log?.body as string) ?? '';
|
||||
return log?.body || '';
|
||||
}
|
||||
// Feature enabled: body is always a map; message is always a string
|
||||
const bodyObj = log?.body as ILogBody;
|
||||
if (!bodyObj) {
|
||||
return '';
|
||||
|
||||
try {
|
||||
const json = JSON.parse(log?.body || '');
|
||||
|
||||
if (typeof json?.message === 'string' && json.message !== '') {
|
||||
return json.message;
|
||||
}
|
||||
|
||||
return log?.body || '';
|
||||
} catch {
|
||||
return log?.body || '';
|
||||
}
|
||||
if (bodyObj.message) {
|
||||
return bodyObj.message;
|
||||
}
|
||||
return JSON.stringify(bodyObj);
|
||||
}, [isBodyJsonQueryEnabled, log?.body]);
|
||||
|
||||
const htmlBody = useMemo(
|
||||
|
||||
@@ -9,10 +9,7 @@ import { Color } from '@signozhq/design-tokens';
|
||||
import { Tooltip } from 'antd';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import {
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
// hooks
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -102,7 +99,7 @@ function RawLogView({
|
||||
// Check if body is selected
|
||||
const showBody = selectedFields.some((field) => field.name === 'body');
|
||||
if (showBody) {
|
||||
parts.push(`${attributesText} ${getBodyDisplayString(data.body)}`);
|
||||
parts.push(`${attributesText} ${data.body}`);
|
||||
} else {
|
||||
parts.push(attributesText);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { ReactElement } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import {
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
@@ -90,7 +87,7 @@ export function useLogsTableColumns({
|
||||
? {
|
||||
id: 'body',
|
||||
header: 'Body',
|
||||
accessorFn: (log): string => getBodyDisplayString(log.body),
|
||||
accessorFn: (log): string => log.body,
|
||||
canBeHidden: false,
|
||||
width: { default: '100%', min: 300 },
|
||||
cell: ({ value, isActive }): ReactElement => (
|
||||
|
||||
@@ -28,7 +28,7 @@ const mockKey: ServiceaccounttypesGettableFactorAPIKeyDTO = {
|
||||
id: 'key-1',
|
||||
name: 'Original Key Name',
|
||||
expiresAt: 0,
|
||||
lastObservedAt: null as unknown as string,
|
||||
lastObservedAt: null as unknown as Date,
|
||||
serviceAccountId: 'sa-1',
|
||||
};
|
||||
|
||||
|
||||
@@ -29,14 +29,14 @@ const keys: ServiceaccounttypesGettableFactorAPIKeyDTO[] = [
|
||||
id: 'key-1',
|
||||
name: 'Production Key',
|
||||
expiresAt: 0,
|
||||
lastObservedAt: null as unknown as string,
|
||||
lastObservedAt: null as unknown as Date,
|
||||
serviceAccountId: 'sa-1',
|
||||
},
|
||||
{
|
||||
id: 'key-2',
|
||||
name: 'Staging Key',
|
||||
expiresAt: 1924905600, // 2030-12-31
|
||||
lastObservedAt: '2026-03-10T10:00:00Z',
|
||||
lastObservedAt: new Date('2026-03-10T10:00:00Z'),
|
||||
serviceAccountId: 'sa-1',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -626,10 +626,6 @@ function TanStackTableInner<TData>(
|
||||
onChange={(value): void => {
|
||||
setLimit(+value);
|
||||
pagination.onLimitChange?.(+value);
|
||||
if (page !== 1) {
|
||||
setPage(1);
|
||||
pagination.onPageChange?.(1);
|
||||
}
|
||||
}}
|
||||
items={paginationPageSizeItems}
|
||||
/>
|
||||
|
||||
@@ -401,62 +401,6 @@ describe('TanStackTableView Integration', () => {
|
||||
expect(onLimitChange).toHaveBeenCalledWith(20);
|
||||
});
|
||||
});
|
||||
|
||||
it('resets page to 1 when limit changes', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
|
||||
const onPageChange = jest.fn();
|
||||
|
||||
renderTanStackTable({
|
||||
props: {
|
||||
pagination: { total: 100, defaultPage: 1, defaultLimit: 10, onPageChange },
|
||||
enableQueryParams: true,
|
||||
},
|
||||
onUrlUpdate,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Navigate to page 2
|
||||
const nav = screen.getByRole('navigation');
|
||||
const page2 = Array.from(nav.querySelectorAll('button')).find(
|
||||
(btn) => btn.textContent?.trim() === '2',
|
||||
);
|
||||
if (!page2) {
|
||||
throw new Error('Page 2 button not found in pagination');
|
||||
}
|
||||
await user.click(page2);
|
||||
|
||||
await waitFor(() => {
|
||||
const lastPage = onUrlUpdate.mock.calls
|
||||
.map((call) => call[0].searchParams.get('page'))
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
expect(lastPage).toBe('2');
|
||||
});
|
||||
|
||||
// Change page size
|
||||
const comboboxTrigger = document.querySelector(
|
||||
'button[aria-haspopup="dialog"]',
|
||||
) as HTMLElement;
|
||||
await user.click(comboboxTrigger);
|
||||
|
||||
const option20 = await screen.findByRole('option', { name: '20' });
|
||||
await user.click(option20);
|
||||
|
||||
// Verify page reset to 1 (nuqs removes default values from URL)
|
||||
await waitFor(() => {
|
||||
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
|
||||
const lastPage = lastCall[0].searchParams.get('page');
|
||||
expect(lastPage === '1' || lastPage === null).toBe(true);
|
||||
expect(lastCall[0].searchParams.get('limit')).toBe('20');
|
||||
});
|
||||
|
||||
// Verify onPageChange callback was called with 1
|
||||
expect(onPageChange).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sorting', () => {
|
||||
|
||||
@@ -108,7 +108,4 @@ export const REACT_QUERY_KEY = {
|
||||
|
||||
// Dashboard Grid Card Query Keys
|
||||
DASHBOARD_GRID_CARD_QUERY_RANGE: 'DASHBOARD_GRID_CARD_QUERY_RANGE',
|
||||
|
||||
// Fields Selector Query Keys
|
||||
GET_FIELDS_SELECTOR_SUGGESTIONS: 'GET_FIELDS_SELECTOR_SUGGESTIONS',
|
||||
} as const;
|
||||
|
||||
@@ -29,6 +29,7 @@ const ROUTES = {
|
||||
ALERTS_NEW: '/alerts/new',
|
||||
ALERT_HISTORY: '/alerts/history',
|
||||
ALERT_OVERVIEW: '/alerts/overview',
|
||||
ALERT_TYPE_SELECTION: '/alerts/type-selection',
|
||||
ALL_CHANNELS: '/settings/channels',
|
||||
CHANNELS_NEW: '/settings/channels/new',
|
||||
CHANNELS_EDIT: '/settings/channels/edit/:channelId',
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { History, Maximize2, Minus, Plus, Sparkles, X } from '@signozhq/icons';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import HistorySidebar from '../components/ConversationsList';
|
||||
import ConversationView from '../ConversationView';
|
||||
import { AIAssistantEvents } from '../events';
|
||||
import {
|
||||
normalizePage,
|
||||
useAIAssistantAnalyticsContext,
|
||||
} from '../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../store/useAIAssistantStore';
|
||||
import { VariantContext } from '../VariantContext';
|
||||
|
||||
@@ -31,7 +24,6 @@ import styles from './AIAssistantModal.module.scss';
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function AIAssistantModal(): JSX.Element | null {
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
|
||||
const isOpen = useAIAssistantStore((s) => s.isModalOpen);
|
||||
@@ -44,7 +36,6 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
const startNewConversation = useAIAssistantStore(
|
||||
(s) => s.startNewConversation,
|
||||
);
|
||||
const analyticsCtx = useAIAssistantAnalyticsContext();
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent): void => {
|
||||
@@ -64,10 +55,6 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
} else {
|
||||
startNewConversation();
|
||||
setShowHistory(false);
|
||||
void logEvent(AIAssistantEvents.Opened, {
|
||||
source: 'shortcut',
|
||||
currentPage: normalizePage(pathname),
|
||||
});
|
||||
openModal();
|
||||
}
|
||||
return;
|
||||
@@ -81,7 +68,7 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return (): void => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, openModal, closeModal, startNewConversation, pathname]);
|
||||
}, [isOpen, openModal, closeModal, startNewConversation]);
|
||||
|
||||
// ── Handlers ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -90,28 +77,15 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
return;
|
||||
}
|
||||
closeModal();
|
||||
// Router state tells AIAssistantPage to skip its mount-time Opened fire:
|
||||
// the assistant was already open in the modal, so this is a surface
|
||||
// switch, not a new open.
|
||||
history.push(
|
||||
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
|
||||
{ fromInApp: true },
|
||||
);
|
||||
}, [activeConversationId, closeModal, history]);
|
||||
|
||||
const handleNew = useCallback(() => {
|
||||
void logEvent(AIAssistantEvents.NewChatClicked, {
|
||||
...analyticsCtx,
|
||||
// useAIAssistantAnalyticsContext() runs above this component's
|
||||
// VariantContext.Provider, so the hook reports the default 'page'
|
||||
// mode. Override here: the modal collapses to 'sidepane' in our
|
||||
// taxonomy alongside the drawer.
|
||||
mode: 'sidepane',
|
||||
source: 'header',
|
||||
});
|
||||
startNewConversation();
|
||||
setShowHistory(false);
|
||||
}, [startNewConversation, analyticsCtx]);
|
||||
}, [startNewConversation]);
|
||||
|
||||
const handleHistorySelect = useCallback(() => {
|
||||
setShowHistory(false);
|
||||
|
||||
@@ -5,12 +5,8 @@ import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { History, Maximize2, Plus, Sparkles, X } from '@signozhq/icons';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import ConversationsList from '../components/ConversationsList';
|
||||
import ConversationView from '../ConversationView';
|
||||
import { AIAssistantEvents } from '../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../store/useAIAssistantStore';
|
||||
import { VariantContext } from '../VariantContext';
|
||||
|
||||
@@ -36,35 +32,21 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
const startNewConversation = useAIAssistantStore(
|
||||
(s) => s.startNewConversation,
|
||||
);
|
||||
const analyticsCtx = useAIAssistantAnalyticsContext();
|
||||
|
||||
const handleExpand = useCallback(() => {
|
||||
if (!activeConversationId) {
|
||||
return;
|
||||
}
|
||||
closeDrawer();
|
||||
// Router state tells AIAssistantPage to skip its mount-time Opened fire:
|
||||
// the assistant was already open in the drawer, so this is a surface
|
||||
// switch, not a new open.
|
||||
history.push(
|
||||
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
|
||||
{ fromInApp: true },
|
||||
);
|
||||
}, [activeConversationId, closeDrawer, history]);
|
||||
|
||||
const handleNew = useCallback(() => {
|
||||
void logEvent(AIAssistantEvents.NewChatClicked, {
|
||||
...analyticsCtx,
|
||||
// useAIAssistantAnalyticsContext() runs above this component's
|
||||
// VariantContext.Provider, so the hook reports the default 'page'
|
||||
// mode. Override here: this handler only runs when the drawer
|
||||
// itself is mounted, which is unambiguously the sidepane surface.
|
||||
mode: 'sidepane',
|
||||
source: 'header',
|
||||
});
|
||||
startNewConversation();
|
||||
setShowHistory(false);
|
||||
}, [startNewConversation, analyticsCtx]);
|
||||
}, [startNewConversation]);
|
||||
|
||||
// When user picks a conversation from the list, close the sidebar
|
||||
const handleHistorySelect = useCallback(() => {
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { useCallback } from 'react';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { Bot } from '@signozhq/icons';
|
||||
|
||||
import { AIAssistantEvents } from '../events';
|
||||
import { normalizePage } from '../hooks/useAIAssistantAnalyticsContext';
|
||||
import {
|
||||
openAIAssistant,
|
||||
useAIAssistantStore,
|
||||
@@ -29,14 +25,6 @@ export default function AIAssistantTrigger(): JSX.Element | null {
|
||||
exact: true,
|
||||
});
|
||||
|
||||
const handleOpen = useCallback((): void => {
|
||||
void logEvent(AIAssistantEvents.Opened, {
|
||||
source: 'icon',
|
||||
currentPage: normalizePage(pathname),
|
||||
});
|
||||
openAIAssistant();
|
||||
}, [pathname]);
|
||||
|
||||
if (isDrawerOpen || isModalOpen || isFullScreenPage) {
|
||||
return null;
|
||||
}
|
||||
@@ -47,7 +35,7 @@ export default function AIAssistantTrigger(): JSX.Element | null {
|
||||
variant="solid"
|
||||
color="primary"
|
||||
className={styles.trigger}
|
||||
onClick={handleOpen}
|
||||
onClick={openAIAssistant}
|
||||
aria-label="Open AI Assistant"
|
||||
>
|
||||
<Bot size={20} />
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import cx from 'classnames';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import ChatInput, { autoContextKey } from '../components/ChatInput';
|
||||
import ConversationSkeleton from '../components/ConversationSkeleton';
|
||||
import VirtualizedMessages from '../components/VirtualizedMessages';
|
||||
import { AIAssistantEvents } from '../events';
|
||||
import { getAutoContexts } from '../getAutoContexts';
|
||||
import { useAIAssistantAnalyticsContext } from '../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../store/useAIAssistantStore';
|
||||
import { MessageAttachment } from '../types';
|
||||
import { MessageContext } from '../../../api/ai-assistant/chat';
|
||||
@@ -43,7 +39,6 @@ export default function ConversationView({
|
||||
);
|
||||
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
|
||||
const cancelStream = useAIAssistantStore((s) => s.cancelStream);
|
||||
const analyticsCtx = useAIAssistantAnalyticsContext(conversationId);
|
||||
|
||||
// Auto-derived contexts come from the route the user is currently looking
|
||||
// at (dashboard detail, service metrics, an explorer, …). Skip when the
|
||||
@@ -87,50 +82,14 @@ export default function ConversationView({
|
||||
attachments?: MessageAttachment[],
|
||||
contexts?: MessageContext[],
|
||||
) => {
|
||||
const hasAuto = contexts?.some((c) => c.source === 'auto') ?? false;
|
||||
const hasManual = contexts?.some((c) => c.source === 'mention') ?? false;
|
||||
let contextType: 'manual' | 'auto' | 'both' | undefined;
|
||||
if (hasAuto && hasManual) {
|
||||
contextType = 'both';
|
||||
} else if (hasAuto) {
|
||||
contextType = 'auto';
|
||||
} else if (hasManual) {
|
||||
contextType = 'manual';
|
||||
}
|
||||
void logEvent(AIAssistantEvents.MessageSent, {
|
||||
...analyticsCtx,
|
||||
queryLength: text.length,
|
||||
hasContext: hasAuto || hasManual,
|
||||
contextType,
|
||||
respondingToClarification: Boolean(pendingClarificationHere),
|
||||
});
|
||||
void sendMessage(text, attachments, contexts);
|
||||
},
|
||||
[sendMessage, analyticsCtx, pendingClarificationHere],
|
||||
[sendMessage],
|
||||
);
|
||||
|
||||
// Wall-clock timestamp of the current streaming start, used to compute
|
||||
// `secondsSinceStart` on Cancel clicked. Cleared whenever streaming ends.
|
||||
const streamStartedAtRef = useRef<number | null>(null);
|
||||
useEffect(() => {
|
||||
if (!isStreamingHere) {
|
||||
streamStartedAtRef.current = null;
|
||||
return;
|
||||
}
|
||||
if (streamStartedAtRef.current === null) {
|
||||
streamStartedAtRef.current = Date.now();
|
||||
}
|
||||
}, [isStreamingHere]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
const startedAt = streamStartedAtRef.current;
|
||||
void logEvent(AIAssistantEvents.CancelClicked, {
|
||||
threadId: analyticsCtx.threadId,
|
||||
secondsSinceStart:
|
||||
startedAt !== null ? Math.round((Date.now() - startedAt) / 1000) : null,
|
||||
});
|
||||
cancelStream(conversationId);
|
||||
}, [cancelStream, conversationId, analyticsCtx.threadId]);
|
||||
}, [cancelStream, conversationId]);
|
||||
|
||||
const messages = conversation?.messages ?? [];
|
||||
const showDisclaimer = messages.length > 0;
|
||||
@@ -175,7 +134,6 @@ export default function ConversationView({
|
||||
conversationId={conversationId}
|
||||
messages={messages}
|
||||
isStreaming={isStreamingHere}
|
||||
onSendSuggestedPrompt={(text): void => handleSend(text)}
|
||||
/>
|
||||
{showDisclaimer && (
|
||||
<div className={disclaimerClass} role="note" aria-live="polite">
|
||||
|
||||
@@ -41,68 +41,12 @@ import {
|
||||
Undo,
|
||||
} from '@signozhq/icons';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import { AIAssistantEvents, SuggestedPromptCategory } from '../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
|
||||
|
||||
import styles from './ActionsSection.module.scss';
|
||||
|
||||
interface ActionsSectionProps {
|
||||
actions: MessageActionDTO[];
|
||||
/** ID of the assistant message these actions belong to — used in analytics. */
|
||||
messageId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource-type strings the backend uses for `open_resource` and rollback
|
||||
* actions. Centralized here so the route/module lookups below stay in sync.
|
||||
*/
|
||||
const ResourceType = {
|
||||
dashboard: 'dashboard',
|
||||
alert: 'alert',
|
||||
service: 'service',
|
||||
saved_view: 'saved_view',
|
||||
logs_explorer: 'logs_explorer',
|
||||
traces_explorer: 'traces_explorer',
|
||||
metrics_explorer: 'metrics_explorer',
|
||||
} as const;
|
||||
|
||||
/** Maps an open_resource action's resourceType to its product module name. */
|
||||
function targetModuleForResource(resourceType: string): string | null {
|
||||
switch (resourceType) {
|
||||
case ResourceType.dashboard:
|
||||
return 'dashboards';
|
||||
case ResourceType.alert:
|
||||
return 'alerts';
|
||||
case ResourceType.service:
|
||||
return 'apm';
|
||||
case ResourceType.saved_view:
|
||||
return 'savedViews';
|
||||
case ResourceType.logs_explorer:
|
||||
return 'logs';
|
||||
case ResourceType.traces_explorer:
|
||||
return 'traces';
|
||||
case ResourceType.metrics_explorer:
|
||||
return 'metrics';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps an apply_filter signal to its product module name. */
|
||||
function targetModuleForSignal(signal: ApplyFilterSignalDTO): string | null {
|
||||
switch (signal) {
|
||||
case ApplyFilterSignalDTO.logs:
|
||||
return 'logs';
|
||||
case ApplyFilterSignalDTO.traces:
|
||||
return 'traces';
|
||||
case ApplyFilterSignalDTO.metrics:
|
||||
return 'metrics';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
type ChipState = 'idle' | 'loading' | 'success' | 'error';
|
||||
@@ -150,23 +94,23 @@ function resourceRoute(
|
||||
resourceId: string,
|
||||
): string | null {
|
||||
switch (resourceType) {
|
||||
case ResourceType.dashboard:
|
||||
case 'dashboard':
|
||||
return ROUTES.DASHBOARD.replace(':dashboardId', resourceId);
|
||||
case ResourceType.alert: {
|
||||
case 'alert': {
|
||||
const params = new URLSearchParams({ [QueryParams.ruleId]: resourceId });
|
||||
return `${ROUTES.EDIT_ALERTS}?${params.toString()}`;
|
||||
}
|
||||
case ResourceType.service:
|
||||
case 'service':
|
||||
return ROUTES.SERVICE_METRICS.replace(':servicename', resourceId);
|
||||
case ResourceType.saved_view:
|
||||
case 'saved_view':
|
||||
// No detail route — saved views land on the list page.
|
||||
// Caller may provide signal-aware metadata in future; default to logs.
|
||||
return ROUTES.LOGS_SAVE_VIEWS;
|
||||
case ResourceType.logs_explorer:
|
||||
case 'logs_explorer':
|
||||
return ROUTES.LOGS_EXPLORER;
|
||||
case ResourceType.traces_explorer:
|
||||
case 'traces_explorer':
|
||||
return ROUTES.TRACES_EXPLORER;
|
||||
case ResourceType.metrics_explorer:
|
||||
case 'metrics_explorer':
|
||||
return ROUTES.METRICS_EXPLORER_EXPLORER;
|
||||
default:
|
||||
return null;
|
||||
@@ -280,24 +224,6 @@ function actionKey(action: MessageActionDTO, index: number): string {
|
||||
: `${action.kind}:${action.label}:${index}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the prompt to send for a follow_up action. The chip's `label` is
|
||||
* the short display text (e.g. "Python setup"); the real prompt lives in
|
||||
* `input.intent` per the schema doc. Falls back to label defensively so a
|
||||
* malformed server payload doesn't drop the click silently. Both branches
|
||||
* are trimmed so whitespace-only payloads don't become whitespace messages.
|
||||
*/
|
||||
function followUpIntent(action: MessageActionDTO): string {
|
||||
const intent = action.input?.intent;
|
||||
if (typeof intent === 'string') {
|
||||
const trimmed = intent.trim();
|
||||
if (trimmed.length > 0) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
return action.label.trim();
|
||||
}
|
||||
|
||||
/** Maps a signal to its target explorer route. */
|
||||
function explorerRouteForSignal(signal: ApplyFilterSignalDTO): string | null {
|
||||
switch (signal) {
|
||||
@@ -427,12 +353,10 @@ function rollbackCall(
|
||||
*/
|
||||
export default function ActionsSection({
|
||||
actions,
|
||||
messageId,
|
||||
}: ActionsSectionProps): JSX.Element | null {
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
|
||||
const { threadId, page, mode } = useAIAssistantAnalyticsContext();
|
||||
const { redirectWithQueryBuilderData, handleSetQueryData } = useQueryBuilder();
|
||||
|
||||
// Per-chip click state, keyed by chip key (see `key` below). Persists
|
||||
@@ -506,39 +430,13 @@ export default function ActionsSection({
|
||||
switch (action.kind) {
|
||||
case MessageActionKindDTO.open_docs: {
|
||||
if (action.url) {
|
||||
void logEvent(AIAssistantEvents.DocOpened, {
|
||||
threadId,
|
||||
messageId,
|
||||
docPath: action.url,
|
||||
});
|
||||
openInNewTab(action.url);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageActionKindDTO.follow_up: {
|
||||
const intent = followUpIntent(action);
|
||||
if (intent) {
|
||||
// Fire SuggestedPromptClicked + MessageSent so analytics can compute
|
||||
// both the click-through rate against follow-ups offered *and* keep
|
||||
// the unified send funnel intact. `category` distinguishes server-
|
||||
// emitted follow-ups from the empty-state grid. `promptId` stays the
|
||||
// label so dashboards group identical chip texts together regardless
|
||||
// of the dynamic intent payload.
|
||||
void logEvent(AIAssistantEvents.SuggestedPromptClicked, {
|
||||
threadId,
|
||||
messageId,
|
||||
promptId: action.label,
|
||||
category: SuggestedPromptCategory.FollowUp,
|
||||
});
|
||||
void logEvent(AIAssistantEvents.MessageSent, {
|
||||
threadId,
|
||||
page,
|
||||
mode,
|
||||
queryLength: intent.length,
|
||||
hasContext: false,
|
||||
respondingToClarification: false,
|
||||
});
|
||||
void sendMessage(intent);
|
||||
if (action.label) {
|
||||
void sendMessage(action.label);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -546,12 +444,6 @@ export default function ActionsSection({
|
||||
if (action.resourceType && action.resourceId) {
|
||||
const path = resourceRoute(action.resourceType, action.resourceId);
|
||||
if (path) {
|
||||
void logEvent(AIAssistantEvents.ResourceOpened, {
|
||||
threadId,
|
||||
messageId,
|
||||
targetModule: targetModuleForResource(action.resourceType),
|
||||
resourceId: action.resourceId,
|
||||
});
|
||||
history.push(path);
|
||||
}
|
||||
}
|
||||
@@ -564,13 +456,6 @@ export default function ActionsSection({
|
||||
break;
|
||||
}
|
||||
case MessageActionKindDTO.apply_filter: {
|
||||
if (action.signal) {
|
||||
void logEvent(AIAssistantEvents.ApplyFilterClicked, {
|
||||
threadId,
|
||||
messageId,
|
||||
targetModule: targetModuleForSignal(action.signal),
|
||||
});
|
||||
}
|
||||
applyFilter(action, {
|
||||
history,
|
||||
pathname,
|
||||
|
||||
@@ -5,17 +5,13 @@ import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import type { UploadFile } from 'antd';
|
||||
import getSessionStorage from 'api/browser/sessionstorage/get';
|
||||
import setSessionStorage from 'api/browser/sessionstorage/set';
|
||||
import {
|
||||
getListRulesQueryKey,
|
||||
useListRules,
|
||||
} from 'api/generated/services/rules';
|
||||
import type { ListRules200 } from 'api/generated/services/sigNoz.schemas';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
import { useQueryService } from 'hooks/useQueryService';
|
||||
@@ -26,8 +22,6 @@ import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { AIAssistantEvents, getBrowserInfo } from '../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useSpeechRecognition } from '../../hooks/useSpeechRecognition';
|
||||
import { MessageAttachment } from '../../types';
|
||||
import { MessageContext } from '../../../../api/ai-assistant/chat';
|
||||
@@ -143,8 +137,6 @@ function autoContextCategory(ctx: MessageContext): string {
|
||||
const MAX_INPUT_LENGTH = 20000;
|
||||
const WARNING_THRESHOLD = 15000;
|
||||
const HOME_SERVICES_INTERVAL = 30 * 60 * 1000;
|
||||
/** sessionStorage key for the "voice input failed this tab" flag. */
|
||||
const VOICE_UNAVAILABLE_KEY = 'ai-assistant-voice-unavailable';
|
||||
|
||||
const CONTEXT_CATEGORIES = ['Dashboards', 'Alerts', 'Services'] as const;
|
||||
|
||||
@@ -376,28 +368,6 @@ export default function ChatInput({
|
||||
|
||||
// ── Voice input ────────────────────────────────────────────────────────────
|
||||
|
||||
const analyticsCtx = useAIAssistantAnalyticsContext();
|
||||
// Captured at the start of a voice session, consumed when it ends.
|
||||
// Tracks both the trigger (button vs. PTT shortcut) and the wall-clock
|
||||
// start time so we can attribute `durationMs` on the Voice input used
|
||||
// event regardless of which control ended the session.
|
||||
const voiceStartedAtRef = useRef<number | null>(null);
|
||||
const voiceSourceRef = useRef<'button' | 'shortcut' | null>(null);
|
||||
// Set to true after a `network`, `not-allowed`, or `not-supported` failure
|
||||
// so we hide the mic button for the rest of the tab session — silent
|
||||
// retries don't help, and Chromium derivatives without the Google Speech
|
||||
// API key always fail with `network` no matter how many times the user
|
||||
// clicks. Persisted to sessionStorage so a page reload doesn't surface the
|
||||
// button again (closing the tab still resets, in case the user fixed
|
||||
// permissions or switched browsers).
|
||||
const [voiceUnavailable, setVoiceUnavailable] = useState(
|
||||
() => getSessionStorage(VOICE_UNAVAILABLE_KEY) === 'true',
|
||||
);
|
||||
const markVoiceUnavailable = useCallback((): void => {
|
||||
setVoiceUnavailable(true);
|
||||
setSessionStorage(VOICE_UNAVAILABLE_KEY, 'true');
|
||||
}, []);
|
||||
|
||||
const {
|
||||
isListening,
|
||||
isSupported,
|
||||
@@ -418,81 +388,9 @@ export default function ChatInput({
|
||||
setText(capText(committedTextRef.current + separator + transcriptText));
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
// Guard against double-fire: Chrome can fire `onerror` more than
|
||||
// once per session when `continuous = true` (it retries internally
|
||||
// before giving up). Only fire the analytics event for the first
|
||||
// error in a given session — voiceSourceRef being null means we've
|
||||
// already handled it.
|
||||
const source = voiceSourceRef.current;
|
||||
if (source === null) {
|
||||
return;
|
||||
}
|
||||
voiceStartedAtRef.current = null;
|
||||
voiceSourceRef.current = null;
|
||||
void logEvent(AIAssistantEvents.VoiceInputFailed, {
|
||||
...analyticsCtx,
|
||||
...getBrowserInfo(),
|
||||
source,
|
||||
errorType: error,
|
||||
});
|
||||
if (error === 'network') {
|
||||
markVoiceUnavailable();
|
||||
toast.error('Voice input unavailable in this browser', {
|
||||
description:
|
||||
'This browser cannot reach the speech recognition service. Try Google Chrome or Microsoft Edge.',
|
||||
});
|
||||
} else if (error === 'not-allowed') {
|
||||
markVoiceUnavailable();
|
||||
toast.error('Microphone access denied', {
|
||||
description:
|
||||
'Grant microphone permission in your browser settings to use voice input.',
|
||||
});
|
||||
} else if (error === 'not-supported') {
|
||||
markVoiceUnavailable();
|
||||
toast.error('Voice input is not supported in this browser.');
|
||||
}
|
||||
// `no-speech` is benign (just silence) — don't toast or hide.
|
||||
},
|
||||
});
|
||||
|
||||
const showMic = isSupported && micPermission !== 'denied' && !voiceUnavailable;
|
||||
|
||||
const startVoiceInput = useCallback(
|
||||
(source: 'button' | 'shortcut') => {
|
||||
// Defense in depth: the button is hidden when `voiceUnavailable` is
|
||||
// true, but the PTT shortcut listener can still call us. Bailing here
|
||||
// keeps a single source of truth and prevents repeat `Voice input
|
||||
// failed` events in the same session.
|
||||
if (voiceUnavailable) {
|
||||
return;
|
||||
}
|
||||
voiceStartedAtRef.current = Date.now();
|
||||
voiceSourceRef.current = source;
|
||||
start();
|
||||
},
|
||||
[start, voiceUnavailable],
|
||||
);
|
||||
|
||||
const fireVoiceInputEvent = useCallback(
|
||||
(outcome: 'sent' | 'discarded') => {
|
||||
const startedAt = voiceStartedAtRef.current;
|
||||
const source = voiceSourceRef.current;
|
||||
voiceStartedAtRef.current = null;
|
||||
voiceSourceRef.current = null;
|
||||
if (startedAt === null || source === null) {
|
||||
return;
|
||||
}
|
||||
void logEvent(AIAssistantEvents.VoiceInputUsed, {
|
||||
...analyticsCtx,
|
||||
...getBrowserInfo(),
|
||||
source,
|
||||
outcome,
|
||||
durationMs: Date.now() - startedAt,
|
||||
});
|
||||
},
|
||||
[analyticsCtx],
|
||||
);
|
||||
const showMic = isSupported && micPermission !== 'denied';
|
||||
|
||||
// Stop recording and immediately send whatever is in the textarea.
|
||||
const handleStopAndSend = useCallback(async () => {
|
||||
@@ -500,17 +398,15 @@ export default function ChatInput({
|
||||
committedTextRef.current = capText(text);
|
||||
// Stop recognition without triggering onTranscript again (would double-append).
|
||||
discard();
|
||||
fireVoiceInputEvent('sent');
|
||||
await handleSend();
|
||||
}, [text, discard, handleSend, capText, fireVoiceInputEvent]);
|
||||
}, [text, discard, handleSend, capText]);
|
||||
|
||||
// Stop recording and revert the textarea to what it was before voice started.
|
||||
const handleDiscard = useCallback(() => {
|
||||
discard();
|
||||
fireVoiceInputEvent('discarded');
|
||||
setText(committedTextRef.current);
|
||||
textareaRef.current?.focus();
|
||||
}, [discard, fireVoiceInputEvent]);
|
||||
}, [discard]);
|
||||
|
||||
// ── Push-to-talk (Cmd/Ctrl + Shift + Space) ────────────────────────────────
|
||||
// Hold the combo to record; release Space to submit. We track which key
|
||||
@@ -519,7 +415,7 @@ export default function ChatInput({
|
||||
// "session active" ref so a held key only calls `start()` once.
|
||||
const pttActiveRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!isSupported || micPermission === 'denied' || voiceUnavailable) {
|
||||
if (!isSupported || micPermission === 'denied') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -536,7 +432,7 @@ export default function ChatInput({
|
||||
return; // ignore auto-repeat
|
||||
}
|
||||
pttActiveRef.current = true;
|
||||
startVoiceInput('shortcut');
|
||||
start();
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent): void => {
|
||||
@@ -570,10 +466,9 @@ export default function ChatInput({
|
||||
}, [
|
||||
isSupported,
|
||||
micPermission,
|
||||
voiceUnavailable,
|
||||
disabled,
|
||||
isStreaming,
|
||||
startVoiceInput,
|
||||
start,
|
||||
handleStopAndSend,
|
||||
]);
|
||||
|
||||
@@ -1008,7 +903,7 @@ export default function ChatInput({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(): void => startVoiceInput('button')}
|
||||
onClick={start}
|
||||
disabled={disabled}
|
||||
aria-label="Start voice input"
|
||||
className={styles.micBtn}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from '@signozhq/ui/select';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ClarificationFieldTypeDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
|
||||
import type {
|
||||
ClarificationEventDTO,
|
||||
@@ -17,8 +16,6 @@ import type {
|
||||
} from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
|
||||
import { CircleHelp, Send, X } from '@signozhq/icons';
|
||||
|
||||
import { AIAssistantEvents } from '../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
|
||||
|
||||
import styles from './ClarificationForm.module.scss';
|
||||
@@ -47,8 +44,6 @@ export default function ClarificationForm({
|
||||
const isStreaming = useAIAssistantStore(
|
||||
(s) => s.streams[conversationId]?.isStreaming ?? false,
|
||||
);
|
||||
const { threadId, page, mode } =
|
||||
useAIAssistantAnalyticsContext(conversationId);
|
||||
|
||||
const fields = clarification.fields ?? [];
|
||||
const initialAnswers = Object.fromEntries(
|
||||
@@ -65,18 +60,6 @@ export default function ClarificationForm({
|
||||
|
||||
const handleSubmit = async (): Promise<void> => {
|
||||
setSubmitted(true);
|
||||
// Approximate queryLength as the JSON encoding of the form answers — the
|
||||
// clarification API doesn't render a single user-visible string, but the
|
||||
// JSON size is a reasonable stand-in for "how much did the user provide".
|
||||
const queryLength = JSON.stringify(answers).length;
|
||||
void logEvent(AIAssistantEvents.MessageSent, {
|
||||
threadId,
|
||||
page,
|
||||
mode,
|
||||
queryLength,
|
||||
hasContext: false,
|
||||
respondingToClarification: true,
|
||||
});
|
||||
await submitClarification(
|
||||
conversationId,
|
||||
clarification.clarificationId,
|
||||
@@ -86,10 +69,6 @@ export default function ClarificationForm({
|
||||
|
||||
const handleCancel = (): void => {
|
||||
setCancelled(true);
|
||||
void logEvent(AIAssistantEvents.CancelClicked, {
|
||||
threadId,
|
||||
secondsSinceStart: null,
|
||||
});
|
||||
cancelStream(conversationId);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,9 +5,6 @@ import { Input } from '@signozhq/ui/input';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Plus, Search } from '@signozhq/icons';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import { AIAssistantEvents } from '../../events';
|
||||
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
|
||||
import { Conversation } from '../../types';
|
||||
import { useVariant } from '../../VariantContext';
|
||||
@@ -139,17 +136,6 @@ export default function ConversationsList({
|
||||
|
||||
const handleSelect = (id: string): void => {
|
||||
const conv = conversations[id];
|
||||
// Skip re-selecting the currently active thread — Notion-style click on
|
||||
// the highlighted row in the history list shouldn't inflate the funnel.
|
||||
const isReselectingActive = id === activeConversationId;
|
||||
if (conv?.threadId && !isReselectingActive) {
|
||||
void logEvent(AIAssistantEvents.ThreadOpenedFromHistory, {
|
||||
threadId: conv.threadId,
|
||||
threadAgeDays: Math.floor(
|
||||
(Date.now() - conv.createdAt) / (24 * 60 * 60 * 1000),
|
||||
),
|
||||
});
|
||||
}
|
||||
if (conv?.threadId) {
|
||||
// Always load from backend — refreshes messages and reconnects
|
||||
// to active execution if the thread is still busy.
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function MessageBubble({
|
||||
)}
|
||||
|
||||
{!isUser && message.actions && message.actions.length > 0 && (
|
||||
<ActionsSection actions={message.actions} messageId={message.id} />
|
||||
<ActionsSection actions={message.actions} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,10 +8,6 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { Check, Copy, RefreshCw, ThumbsDown, ThumbsUp } from '@signozhq/icons';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import { AIAssistantEvents } from '../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
|
||||
import { FeedbackRating, Message } from '../../types';
|
||||
|
||||
@@ -58,7 +54,6 @@ export default function MessageFeedback({
|
||||
const submitMessageFeedback = useAIAssistantStore(
|
||||
(s) => s.submitMessageFeedback,
|
||||
);
|
||||
const { threadId } = useAIAssistantAnalyticsContext();
|
||||
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
@@ -96,21 +91,10 @@ export default function MessageFeedback({
|
||||
}, [message.createdAt]);
|
||||
|
||||
const handleCopy = useCallback((): void => {
|
||||
void logEvent(AIAssistantEvents.MessageCopied, {
|
||||
role: message.role,
|
||||
messageId: message.id,
|
||||
hadToolCalls: Boolean(message.blocks?.some((b) => b.type === 'tool_call')),
|
||||
});
|
||||
copyToClipboard(message.content);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
}, [
|
||||
copyToClipboard,
|
||||
message.content,
|
||||
message.id,
|
||||
message.role,
|
||||
message.blocks,
|
||||
]);
|
||||
}, [copyToClipboard, message.content]);
|
||||
|
||||
const handleVote = useCallback(
|
||||
(rating: FeedbackRating): void => {
|
||||
@@ -123,31 +107,20 @@ export default function MessageFeedback({
|
||||
return;
|
||||
}
|
||||
setVote(rating);
|
||||
void logEvent(AIAssistantEvents.FeedbackSubmitted, {
|
||||
messageId: message.id,
|
||||
threadId,
|
||||
rating: 'up',
|
||||
hasComment: false,
|
||||
commentLength: 0,
|
||||
});
|
||||
submitMessageFeedback(message.id, rating);
|
||||
},
|
||||
[vote, message.id, submitMessageFeedback, threadId],
|
||||
[vote, message.id, submitMessageFeedback],
|
||||
);
|
||||
|
||||
const handleSubmitNegative = useCallback((): void => {
|
||||
setVote('negative');
|
||||
setIsNegativeDialogOpen(false);
|
||||
const trimmed = negativeComment.trim();
|
||||
void logEvent(AIAssistantEvents.FeedbackSubmitted, {
|
||||
messageId: message.id,
|
||||
threadId,
|
||||
rating: 'down',
|
||||
hasComment: trimmed.length > 0,
|
||||
commentLength: trimmed.length,
|
||||
});
|
||||
submitMessageFeedback(message.id, 'negative', trimmed || undefined);
|
||||
}, [message.id, negativeComment, submitMessageFeedback, threadId]);
|
||||
submitMessageFeedback(
|
||||
message.id,
|
||||
'negative',
|
||||
negativeComment.trim() || undefined,
|
||||
);
|
||||
}, [message.id, negativeComment, submitMessageFeedback]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -4,9 +4,6 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Check, Copy } from '@signozhq/icons';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import { AIAssistantEvents } from '../../events';
|
||||
import { Message } from '../../types';
|
||||
|
||||
import styles from './UserMessageActions.module.scss';
|
||||
@@ -28,15 +25,10 @@ export default function UserMessageActions({
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const handleCopy = useCallback((): void => {
|
||||
void logEvent(AIAssistantEvents.MessageCopied, {
|
||||
role: message.role,
|
||||
messageId: message.id,
|
||||
hadToolCalls: false,
|
||||
});
|
||||
copyToClipboard(message.content);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
}, [copyToClipboard, message.content, message.id, message.role]);
|
||||
}, [copyToClipboard, message.content]);
|
||||
|
||||
return (
|
||||
<div className={styles.actions}>
|
||||
|
||||
@@ -10,10 +10,6 @@ import {
|
||||
Sparkles,
|
||||
} from '@signozhq/icons';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import { AIAssistantEvents, SuggestedPromptCategory } from '../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
|
||||
import { Message, StreamingEventItem } from '../../types';
|
||||
import MessageBubble from '../MessageBubble';
|
||||
@@ -50,24 +46,17 @@ interface VirtualizedMessagesProps {
|
||||
conversationId: string;
|
||||
messages: Message[];
|
||||
isStreaming: boolean;
|
||||
/**
|
||||
* Called when a user clicks an empty-state suggested prompt. Routed
|
||||
* through the parent so analytics (Message sent) fire with the same
|
||||
* page/mode/context attribution as a normal send.
|
||||
*/
|
||||
onSendSuggestedPrompt: (text: string) => void;
|
||||
}
|
||||
|
||||
export default function VirtualizedMessages({
|
||||
conversationId,
|
||||
messages,
|
||||
isStreaming,
|
||||
onSendSuggestedPrompt,
|
||||
}: VirtualizedMessagesProps): JSX.Element {
|
||||
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
|
||||
const regenerateAssistantMessage = useAIAssistantStore(
|
||||
(s) => s.regenerateAssistantMessage,
|
||||
);
|
||||
const { threadId } = useAIAssistantAnalyticsContext(conversationId);
|
||||
const streamingStatus = useAIAssistantStore(
|
||||
(s) => s.streams[conversationId]?.streamingStatus ?? '',
|
||||
);
|
||||
@@ -96,13 +85,9 @@ export default function VirtualizedMessages({
|
||||
if (isStreaming) {
|
||||
return;
|
||||
}
|
||||
void logEvent(AIAssistantEvents.RegenerateClicked, {
|
||||
messageId,
|
||||
threadId,
|
||||
});
|
||||
void regenerateAssistantMessage(conversationId, messageId);
|
||||
},
|
||||
[conversationId, isStreaming, regenerateAssistantMessage, threadId],
|
||||
[conversationId, isStreaming, regenerateAssistantMessage],
|
||||
);
|
||||
|
||||
// Scroll all the way to the actual bottom — including the 64px of bottom
|
||||
@@ -161,11 +146,7 @@ export default function VirtualizedMessages({
|
||||
color="secondary"
|
||||
className={styles.emptyChip}
|
||||
onClick={(): void => {
|
||||
void logEvent(AIAssistantEvents.SuggestedPromptClicked, {
|
||||
promptId: s.text,
|
||||
category: SuggestedPromptCategory.EmptyState,
|
||||
});
|
||||
onSendSuggestedPrompt(s.text);
|
||||
sendMessage(s.text);
|
||||
}}
|
||||
prefix={<s.icon size={14} />}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import cx from 'classnames';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { Check, X } from '@signozhq/icons';
|
||||
|
||||
import { AIAssistantEvents } from '../../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../../../store/useAIAssistantStore';
|
||||
import { useMessageContext } from '../../MessageContext';
|
||||
|
||||
@@ -40,7 +37,6 @@ export default function ConfirmBlock({
|
||||
const answeredBlocks = useAIAssistantStore((s) => s.answeredBlocks);
|
||||
const markBlockAnswered = useAIAssistantStore((s) => s.markBlockAnswered);
|
||||
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
|
||||
const { threadId, page, mode } = useAIAssistantAnalyticsContext();
|
||||
|
||||
// Durable answered state — survives re-renders/remounts
|
||||
const answeredChoice = messageId ? answeredBlocks[messageId] : undefined;
|
||||
@@ -51,14 +47,6 @@ export default function ConfirmBlock({
|
||||
if (messageId) {
|
||||
markBlockAnswered(messageId, choice);
|
||||
}
|
||||
void logEvent(AIAssistantEvents.MessageSent, {
|
||||
threadId,
|
||||
page,
|
||||
mode,
|
||||
queryLength: responseText.length,
|
||||
hasContext: false,
|
||||
respondingToClarification: false,
|
||||
});
|
||||
sendMessage(responseText);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { Checkbox, Radio } from 'antd';
|
||||
|
||||
import { AIAssistantEvents } from '../../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../../../store/useAIAssistantStore';
|
||||
import { useMessageContext } from '../../MessageContext';
|
||||
|
||||
@@ -39,7 +36,6 @@ export default function InteractiveQuestion({
|
||||
const answeredBlocks = useAIAssistantStore((s) => s.answeredBlocks);
|
||||
const markBlockAnswered = useAIAssistantStore((s) => s.markBlockAnswered);
|
||||
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
|
||||
const { threadId, page, mode } = useAIAssistantAnalyticsContext();
|
||||
|
||||
// Persist selected state locally only for the pending (not-yet-submitted) case
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
@@ -56,14 +52,6 @@ export default function InteractiveQuestion({
|
||||
if (messageId) {
|
||||
markBlockAnswered(messageId, answer);
|
||||
}
|
||||
void logEvent(AIAssistantEvents.MessageSent, {
|
||||
threadId,
|
||||
page,
|
||||
mode,
|
||||
queryLength: answer.length,
|
||||
hasContext: false,
|
||||
respondingToClarification: false,
|
||||
});
|
||||
sendMessage(answer);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* Analytics event names for the AI Assistant feature. Backend-emitted events
|
||||
* (Execution finished, Approval resolved, Resource mutated, Clarification
|
||||
* requested, Limit hit) are not declared here — they fire from the AI service.
|
||||
*/
|
||||
|
||||
export interface BrowserInfo {
|
||||
browserName: string;
|
||||
browserVersion: string;
|
||||
}
|
||||
|
||||
type NavigatorWithBrandHints = Navigator & {
|
||||
userAgentData?: { brands: { brand: string; version: string }[] };
|
||||
brave?: { isBrave: () => Promise<boolean> };
|
||||
};
|
||||
|
||||
/**
|
||||
* We mainly need to distinguish Chrome / Edge (Speech API works) from Chromium
|
||||
* derivatives (no Google API key → voice fails with `network`). UA sniffing is
|
||||
* the source of truth for derivative identification; `userAgentData` is used
|
||||
* only as a fast happy path for Chrome / Edge. Brave needs its own probe — it
|
||||
* advertises Chrome in both UA and brand hints.
|
||||
*/
|
||||
export function getBrowserInfo(): BrowserInfo {
|
||||
if (typeof navigator === 'undefined') {
|
||||
return { browserName: 'unknown', browserVersion: 'unknown' };
|
||||
}
|
||||
const nav = navigator as NavigatorWithBrandHints;
|
||||
const ua = nav.userAgent;
|
||||
|
||||
// Order matters: derivatives put "Chrome" in their UA; Chrome puts "Safari".
|
||||
const matchers: { name: string; re: RegExp }[] = [
|
||||
{ name: 'Edge', re: /Edg(?:e|A|iOS)?\/([\d.]+)/ },
|
||||
{ name: 'Opera', re: /OPR\/([\d.]+)/ },
|
||||
{ name: 'Vivaldi', re: /Vivaldi\/([\d.]+)/ },
|
||||
{ name: 'Chrome', re: /Chrome\/([\d.]+)/ },
|
||||
{ name: 'Firefox', re: /Firefox\/([\d.]+)/ },
|
||||
{ name: 'Safari', re: /Version\/([\d.]+).*Safari/ },
|
||||
];
|
||||
let browserName = 'unknown';
|
||||
let browserVersion = 'unknown';
|
||||
for (const { name, re } of matchers) {
|
||||
const m = ua.match(re);
|
||||
if (m) {
|
||||
browserName = name;
|
||||
browserVersion = m[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Brave hides as Chrome in UA + brand hints; its probe is the only tell.
|
||||
if (nav.brave?.isBrave) {
|
||||
browserName = 'Brave';
|
||||
}
|
||||
|
||||
return { browserName, browserVersion };
|
||||
}
|
||||
|
||||
export const SuggestedPromptCategory = {
|
||||
FollowUp: 'follow_up',
|
||||
EmptyState: 'empty_state',
|
||||
} as const;
|
||||
export type SuggestedPromptCategory =
|
||||
(typeof SuggestedPromptCategory)[keyof typeof SuggestedPromptCategory];
|
||||
|
||||
export enum AIAssistantEvents {
|
||||
Opened = 'AI Assistant: Opened',
|
||||
MessageSent = 'AI Assistant: Message sent',
|
||||
SuggestedPromptClicked = 'AI Assistant: Suggested prompt clicked',
|
||||
CancelClicked = 'AI Assistant: Cancel clicked',
|
||||
RegenerateClicked = 'AI Assistant: Regenerate clicked',
|
||||
MessageCopied = 'AI Assistant: Message copied',
|
||||
FeedbackSubmitted = 'AI Assistant: Feedback submitted',
|
||||
ResourceOpened = 'AI Assistant: Resource opened',
|
||||
DocOpened = 'AI Assistant: Doc opened',
|
||||
ApplyFilterClicked = 'AI Assistant: Apply filter clicked',
|
||||
ThreadOpenedFromHistory = 'AI Assistant: Thread opened from history',
|
||||
VoiceInputUsed = 'AI Assistant: Voice input used',
|
||||
VoiceInputFailed = 'AI Assistant: Voice input failed',
|
||||
NewChatClicked = 'AI Assistant: New chat clicked',
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
import { useAIAssistantStore } from '../store/useAIAssistantStore';
|
||||
import { useVariant } from '../VariantContext';
|
||||
|
||||
export interface AIAssistantAnalyticsContext {
|
||||
/** Backend thread ID for the resolved conversation; undefined before the first send. */
|
||||
threadId: string | undefined;
|
||||
/**
|
||||
* Normalised route template for the current page (e.g. `/dashboard/:dashboardId`).
|
||||
* Falls back to the raw pathname for routes not in ROUTES. We normalise to keep
|
||||
* analytics cardinality bounded and avoid leaking customer identifiers
|
||||
* (dashboard IDs, service names, trace IDs, conversation IDs) into the event.
|
||||
*/
|
||||
page: string;
|
||||
/** Surface the assistant is rendered on. `panel` / `modal` collapse to `sidepane`. */
|
||||
mode: 'sidepane' | 'full_screen';
|
||||
}
|
||||
|
||||
// Pre-sorted longest-first so more specific templates match before their
|
||||
// less specific siblings (e.g. `/services/:s/top-level-operations` wins
|
||||
// over `/services/:s`). Module-level — ROUTES is static.
|
||||
const ROUTE_TEMPLATES = Object.values(ROUTES).sort(
|
||||
(a, b) => b.length - a.length,
|
||||
);
|
||||
|
||||
export function normalizePage(pathname: string): string {
|
||||
for (const template of ROUTE_TEMPLATES) {
|
||||
if (matchPath(pathname, { path: template, exact: true })) {
|
||||
return template;
|
||||
}
|
||||
}
|
||||
return pathname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared base attributes for AI Assistant analytics events (Message sent,
|
||||
* Cancel clicked, Feedback submitted, Resource/Doc/Apply filter, …).
|
||||
*
|
||||
* Pass `conversationId` when the caller is scoped to a specific
|
||||
* conversation (e.g. `ClarificationForm`, `VirtualizedMessages`); omit
|
||||
* to fall back to the store's active conversation.
|
||||
*/
|
||||
export function useAIAssistantAnalyticsContext(
|
||||
conversationId?: string,
|
||||
): AIAssistantAnalyticsContext {
|
||||
const { pathname } = useLocation();
|
||||
const variant = useVariant();
|
||||
const threadId = useAIAssistantStore((s) => {
|
||||
const id = conversationId ?? s.activeConversationId;
|
||||
return id ? s.conversations[id]?.threadId : undefined;
|
||||
});
|
||||
return {
|
||||
threadId,
|
||||
page: normalizePage(pathname),
|
||||
mode: variant === 'page' ? 'full_screen' : 'sidepane',
|
||||
};
|
||||
}
|
||||
@@ -1,23 +1,12 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import * as usePrefillAlertConditions from 'container/FormAlertRules/usePrefillAlertConditions';
|
||||
import AlertTypeSelectionPage from 'pages/AlertTypeSelection';
|
||||
import CreateAlertPage from 'pages/CreateAlert';
|
||||
import { act, fireEvent, render } from 'tests/test-utils';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { ALERT_TYPE_TO_TITLE, ALERT_TYPE_URL_MAP } from './constants';
|
||||
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
...jest.requireActual('react-router-dom-v5-compat'),
|
||||
useNavigationType: jest.fn(() => 'PUSH'),
|
||||
useLocation: jest.fn(() => ({
|
||||
pathname: '/alerts/new',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
})),
|
||||
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
|
||||
}));
|
||||
|
||||
jest
|
||||
.spyOn(usePrefillAlertConditions, 'usePrefillAlertConditions')
|
||||
.mockReturnValue({
|
||||
@@ -65,13 +54,20 @@ describe('Alert rule documentation redirection', () => {
|
||||
window.open = mockWindowOpen;
|
||||
});
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALERT_TYPE_SELECTION}`,
|
||||
}),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
renderResult = render(
|
||||
<CreateAlertPage />,
|
||||
<AlertTypeSelectionPage />,
|
||||
{},
|
||||
{
|
||||
initialRoute: ROUTES.ALERTS_NEW,
|
||||
initialRoute: ROUTES.ALERT_TYPE_SELECTION,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,18 +15,6 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
...jest.requireActual('react-router-dom-v5-compat'),
|
||||
useNavigationType: jest.fn(() => 'PUSH'),
|
||||
useLocation: jest.fn(() => ({
|
||||
pathname: '/alerts/new',
|
||||
search: 'ruleType=anomaly_rule',
|
||||
hash: '',
|
||||
state: null,
|
||||
})),
|
||||
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
|
||||
}));
|
||||
|
||||
window.ResizeObserver =
|
||||
window.ResizeObserver ||
|
||||
jest.fn().mockImplementation(() => ({
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
|
||||
import * as appHooks from 'providers/App/App';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import SelectAlertType from '..';
|
||||
|
||||
const useAppContextSpy = jest.spyOn(appHooks, 'useAppContext');
|
||||
|
||||
describe('SelectAlertType', () => {
|
||||
const mockOnSelect = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useAppContextSpy.mockReturnValue(getAppContextMockState());
|
||||
});
|
||||
|
||||
it('should render all alert type options when anomaly detection is enabled', () => {
|
||||
useAppContextSpy.mockReturnValue({
|
||||
...getAppContextMockState({}),
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
|
||||
expect(screen.getByText('metric_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('log_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('traces_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('exceptions_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('anomaly_based_alert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all alert type options except anomaly based alert when anomaly detection is disabled', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
|
||||
expect(screen.getByText('metric_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('log_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('traces_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('exceptions_based_alert')).toBeInTheDocument();
|
||||
expect(screen.queryByText('anomaly_based_alert')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onSelect with metrics based alert type', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
fireEvent.click(screen.getByText('metric_based_alert'));
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onSelect with anomaly based alert type', () => {
|
||||
useAppContextSpy.mockReturnValue({
|
||||
...getAppContextMockState({}),
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
fireEvent.click(screen.getByText('anomaly_based_alert'));
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
AlertTypes.ANOMALY_BASED_ALERT,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onSelect with log based alert type', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
fireEvent.click(screen.getByText('log_based_alert'));
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(AlertTypes.LOGS_BASED_ALERT, false);
|
||||
});
|
||||
|
||||
it('should call onSelect with traces based alert type', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
fireEvent.click(screen.getByText('traces_based_alert'));
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
AlertTypes.TRACES_BASED_ALERT,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onSelect with exceptions based alert type', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
fireEvent.click(screen.getByText('exceptions_based_alert'));
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
AlertTypes.EXCEPTIONS_BASED_ALERT,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,37 +1,13 @@
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
|
||||
import * as useCompositeQueryParamHooks from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import * as navigateHooks from 'hooks/useSafeNavigate';
|
||||
import * as useUrlQueryHooks from 'hooks/useUrlQuery';
|
||||
import * as appHooks from 'providers/App/App';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import CreateAlertRule from '../index';
|
||||
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
...jest.requireActual('react-router-dom-v5-compat'),
|
||||
useNavigationType: jest.fn(() => 'PUSH'),
|
||||
useLocation: jest.fn(() => ({
|
||||
pathname: '/alerts/new',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
})),
|
||||
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
|
||||
}));
|
||||
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
||||
__esModule: true,
|
||||
default: function MockDateTimeSelector(): JSX.Element {
|
||||
return <div data-testid="datetime-selector">Mock DateTime Selector</div>;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('container/FormAlertRules', () => ({
|
||||
__esModule: true,
|
||||
default: function MockFormAlertRules({
|
||||
@@ -72,14 +48,10 @@ const useCompositeQueryParamSpy = jest.spyOn(
|
||||
'useGetCompositeQueryParam',
|
||||
);
|
||||
const useUrlQuerySpy = jest.spyOn(useUrlQueryHooks, 'default');
|
||||
const useSafeNavigateSpy = jest.spyOn(navigateHooks, 'useSafeNavigate');
|
||||
const useAppContextSpy = jest.spyOn(appHooks, 'useAppContext');
|
||||
|
||||
const mockSetUrlQuery = jest.fn();
|
||||
const mockToString = jest.fn();
|
||||
const mockGetUrlQuery = jest.fn();
|
||||
const mockSafeNavigate = jest.fn();
|
||||
const mockDeleteUrlQuery = jest.fn();
|
||||
|
||||
const FORM_ALERT_RULES_TEXT = 'Form Alert Rules';
|
||||
const CREATE_ALERT_V2_TEXT = 'Create Alert V2';
|
||||
@@ -91,13 +63,8 @@ describe('CreateAlertRule', () => {
|
||||
set: mockSetUrlQuery,
|
||||
toString: mockToString,
|
||||
get: mockGetUrlQuery,
|
||||
delete: mockDeleteUrlQuery,
|
||||
} as Partial<URLSearchParams> as URLSearchParams);
|
||||
useCompositeQueryParamSpy.mockReturnValue(initialQueriesMap.metrics);
|
||||
useSafeNavigateSpy.mockReturnValue({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
});
|
||||
useAppContextSpy.mockReturnValue(getAppContextMockState());
|
||||
});
|
||||
|
||||
it('should render classic flow when showClassicCreateAlertsPage is true', () => {
|
||||
@@ -105,53 +72,18 @@ describe('CreateAlertRule', () => {
|
||||
if (key === QueryParams.showClassicCreateAlertsPage) {
|
||||
return 'true';
|
||||
}
|
||||
if (key === QueryParams.alertType) {
|
||||
return AlertTypes.METRICS_BASED_ALERT;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render new flow when alertType is provided', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.alertType) {
|
||||
return AlertTypes.METRICS_BASED_ALERT;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
it('should render new flow by default', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render type selection when no alertType in URL and no compositeQuery', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue(null);
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.queryByText(FORM_ALERT_RULES_TEXT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(CREATE_ALERT_V2_TEXT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should skip type selection and render alert form when compositeQuery is present', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue({
|
||||
...initialQueriesMap.metrics,
|
||||
builder: {
|
||||
...initialQueriesMap.metrics.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueriesMap.metrics.builder.queryData[0],
|
||||
dataSource: DataSource.METRICS,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.METRICS_BASED_ALERT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render classic flow when ruleType is anomaly_rule even if showClassicCreateAlertsPage is not true', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.showClassicCreateAlertsPage) {
|
||||
@@ -179,13 +111,8 @@ describe('CreateAlertRule', () => {
|
||||
expect(screen.getByText(AlertTypes.LOGS_BASED_ALERT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use alertType from URL over compositeQuery dataSource', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.alertType) {
|
||||
return AlertTypes.LOGS_BASED_ALERT;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
it('should use alertType from compositeQuery dataSource when alertType is not in URL', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue({
|
||||
...initialQueriesMap.metrics,
|
||||
builder: {
|
||||
@@ -200,123 +127,14 @@ describe('CreateAlertRule', () => {
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.LOGS_BASED_ALERT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.TRACES_BASED_ALERT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('handleSelectType navigation', () => {
|
||||
beforeEach(() => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue(null);
|
||||
});
|
||||
|
||||
it('should navigate with threshold alert params for metrics alert', () => {
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('metric_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'threshold_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate with threshold alert params for logs alert', () => {
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('log_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'threshold_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.LOGS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate with threshold alert params for traces alert', () => {
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('traces_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'threshold_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.TRACES_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate with threshold alert params for exceptions alert', () => {
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('exceptions_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'threshold_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.EXCEPTIONS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate with anomaly detection params for anomaly alert', () => {
|
||||
useAppContextSpy.mockReturnValue({
|
||||
...getAppContextMockState({}),
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('anomaly_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'anomaly_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate even when showClassicCreateAlertsPage flag is present', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.showClassicCreateAlertsPage) {
|
||||
return 'true';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('metric_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'threshold_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
it('should default to METRICS_BASED_ALERT when no alertType and no compositeQuery', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue(null);
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.METRICS_BASED_ALERT)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,11 +208,3 @@ export const ALERTS_VALUES_MAP: Record<AlertTypes, AlertDef> = {
|
||||
[AlertTypes.TRACES_BASED_ALERT]: traceAlertDefaults,
|
||||
[AlertTypes.EXCEPTIONS_BASED_ALERT]: exceptionAlertDefaults,
|
||||
};
|
||||
|
||||
export const ALERT_TYPE_BREADCRUMB_TITLE: Record<AlertTypes, string> = {
|
||||
[AlertTypes.ANOMALY_BASED_ALERT]: 'Anomaly-Based Alert',
|
||||
[AlertTypes.METRICS_BASED_ALERT]: 'Metric-Based Alert',
|
||||
[AlertTypes.LOGS_BASED_ALERT]: 'Log-Based Alert',
|
||||
[AlertTypes.TRACES_BASED_ALERT]: 'Traces-Based Alert',
|
||||
[AlertTypes.EXCEPTIONS_BASED_ALERT]: 'Exceptions-Based Alert',
|
||||
};
|
||||
|
||||
@@ -1,34 +1,21 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Form, Tabs, TabsProps } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon';
|
||||
import AlertBreadcrumb from 'components/AlertBreadcrumb';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import { useMemo } from 'react';
|
||||
import { Form } from 'antd';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import CreateAlertV2 from 'container/CreateAlertV2';
|
||||
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { AlertListTabs } from 'pages/AlertList/types';
|
||||
import { GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
|
||||
import { ALERTS_VALUES_MAP, ALERT_TYPE_BREADCRUMB_TITLE } from './defaults';
|
||||
import SelectAlertType from './SelectAlertType';
|
||||
|
||||
import './CreateAlertRule.styles.scss';
|
||||
import { ALERTS_VALUES_MAP } from './defaults';
|
||||
|
||||
function CreateRules(): JSX.Element {
|
||||
const [formInstance] = Form.useForm();
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
const queryParams = useUrlQuery();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const ruleTypeFromURL = queryParams.get(QueryParams.ruleType);
|
||||
const alertTypeFromURL = queryParams.get(QueryParams.alertType);
|
||||
@@ -36,15 +23,6 @@ function CreateRules(): JSX.Element {
|
||||
const showClassicCreateAlertsPageFlag =
|
||||
queryParams.get(QueryParams.showClassicCreateAlertsPage) === 'true';
|
||||
|
||||
const isTypeSelectionMode =
|
||||
!alertTypeFromURL && !ruleTypeFromURL && !compositeQuery;
|
||||
|
||||
useEffect(() => {
|
||||
if (isTypeSelectionMode) {
|
||||
logEvent('Alert: New alert data source selection page visited', {});
|
||||
}
|
||||
}, [isTypeSelectionMode]);
|
||||
|
||||
const alertType = useMemo(() => {
|
||||
if (ruleTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
|
||||
return AlertTypes.ANOMALY_BASED_ALERT;
|
||||
@@ -67,142 +45,22 @@ function CreateRules(): JSX.Element {
|
||||
[alertType, version],
|
||||
);
|
||||
|
||||
const handleTabChange = useCallback(
|
||||
(tab: string): void => {
|
||||
queryParams.set('tab', tab);
|
||||
queryParams.delete('subTab');
|
||||
queryParams.delete('search');
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${queryParams.toString()}`);
|
||||
},
|
||||
[safeNavigate, queryParams],
|
||||
);
|
||||
// Load old alerts flow always for anomaly based alerts and when showClassicCreateAlertsPage is true
|
||||
if (
|
||||
showClassicCreateAlertsPageFlag ||
|
||||
alertType === AlertTypes.ANOMALY_BASED_ALERT
|
||||
) {
|
||||
return (
|
||||
<FormAlertRules
|
||||
alertType={alertType}
|
||||
formInstance={formInstance}
|
||||
initialValue={initialAlertValue}
|
||||
ruleId=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSelectType = useCallback(
|
||||
(type: AlertTypes, newTab?: boolean): void => {
|
||||
if (type === AlertTypes.ANOMALY_BASED_ALERT) {
|
||||
queryParams.set(
|
||||
QueryParams.ruleType,
|
||||
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||
);
|
||||
queryParams.set(QueryParams.alertType, AlertTypes.METRICS_BASED_ALERT);
|
||||
} else {
|
||||
queryParams.set(QueryParams.ruleType, AlertDetectionTypes.THRESHOLD_ALERT);
|
||||
queryParams.set(QueryParams.alertType, type);
|
||||
}
|
||||
|
||||
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`, { newTab });
|
||||
},
|
||||
[queryParams, safeNavigate],
|
||||
);
|
||||
|
||||
const alertContent = useMemo(() => {
|
||||
if (isTypeSelectionMode) {
|
||||
return <SelectAlertType onSelect={handleSelectType} />;
|
||||
}
|
||||
|
||||
if (
|
||||
showClassicCreateAlertsPageFlag ||
|
||||
alertType === AlertTypes.ANOMALY_BASED_ALERT
|
||||
) {
|
||||
return (
|
||||
<FormAlertRules
|
||||
alertType={alertType}
|
||||
formInstance={formInstance}
|
||||
initialValue={initialAlertValue}
|
||||
ruleId=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <CreateAlertV2 alertType={alertType} />;
|
||||
}, [
|
||||
isTypeSelectionMode,
|
||||
handleSelectType,
|
||||
showClassicCreateAlertsPageFlag,
|
||||
alertType,
|
||||
formInstance,
|
||||
initialAlertValue,
|
||||
]);
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
label: (
|
||||
<div className="periscope-tab top-level-tab">
|
||||
<GalleryVerticalEnd size={14} />
|
||||
Triggered Alerts
|
||||
</div>
|
||||
),
|
||||
key: AlertListTabs.TRIGGERED_ALERTS,
|
||||
children: null,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="periscope-tab top-level-tab">
|
||||
<Pyramid size={14} />
|
||||
Alert Rules
|
||||
</div>
|
||||
),
|
||||
key: AlertListTabs.ALERT_RULES,
|
||||
children: (
|
||||
<div className="create-alert-wrapper">
|
||||
<AlertBreadcrumb
|
||||
className="create-alert__breadcrumb"
|
||||
items={
|
||||
isTypeSelectionMode
|
||||
? [
|
||||
{
|
||||
title: 'Alert Rules',
|
||||
route: `${ROUTES.LIST_ALL_ALERT}?tab=${AlertListTabs.ALERT_RULES}`,
|
||||
},
|
||||
{ title: 'Select Alert Type', isLast: true },
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: 'Alert Rules',
|
||||
route: `${ROUTES.LIST_ALL_ALERT}?tab=${AlertListTabs.ALERT_RULES}`,
|
||||
},
|
||||
{ title: 'Select Alert Type', route: ROUTES.ALERTS_NEW },
|
||||
{
|
||||
title: ALERT_TYPE_BREADCRUMB_TITLE[alertType],
|
||||
isLast: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
{alertContent}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="periscope-tab top-level-tab">
|
||||
<ConfigureIcon width={14} height={14} />
|
||||
Configuration
|
||||
</div>
|
||||
),
|
||||
key: AlertListTabs.CONFIGURATION,
|
||||
children: null,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
destroyInactiveTabPane
|
||||
items={items}
|
||||
activeKey={AlertListTabs.ALERT_RULES}
|
||||
onChange={handleTabChange}
|
||||
className="alerts-container create-alert-tabs"
|
||||
tabBarExtraContent={
|
||||
<div className="create-alert-tabs__extra">
|
||||
<DateTimeSelector showAutoRefresh />
|
||||
<HeaderRightSection
|
||||
enableAnnouncements={false}
|
||||
enableShare
|
||||
enableFeedback
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
return <CreateAlertV2 alertType={alertType} />;
|
||||
}
|
||||
|
||||
export default CreateRules;
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { RotateCcw } from '@signozhq/icons';
|
||||
import { useAlertRuleOptional } from 'providers/Alert';
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
@@ -19,7 +18,6 @@ import './styles.scss';
|
||||
|
||||
function CreateAlertHeader(): JSX.Element {
|
||||
const { alertState, setAlertState, isEditMode } = useCreateAlertState();
|
||||
const alertRuleContext = useAlertRuleOptional();
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
@@ -76,13 +74,9 @@ function CreateAlertHeader(): JSX.Element {
|
||||
<Input
|
||||
type="text"
|
||||
value={alertState.name}
|
||||
onChange={(e): void => {
|
||||
const newName = e.target.value;
|
||||
setAlertState({ type: 'SET_ALERT_NAME', payload: newName });
|
||||
if (isEditMode && alertRuleContext?.setAlertRuleName) {
|
||||
alertRuleContext.setAlertRuleName(newName);
|
||||
}
|
||||
}}
|
||||
onChange={(e): void =>
|
||||
setAlertState({ type: 'SET_ALERT_NAME', payload: e.target.value })
|
||||
}
|
||||
className="alert-header__input title"
|
||||
placeholder="Enter alert rule name"
|
||||
data-testid="alert-name-input"
|
||||
|
||||
@@ -20,11 +20,6 @@ import {
|
||||
} from './utils';
|
||||
|
||||
import './styles.scss';
|
||||
import {
|
||||
invalidateGetRuleByID,
|
||||
invalidateListRules,
|
||||
} from 'api/generated/services/rules';
|
||||
import { useQueryClient } from 'react-query';
|
||||
|
||||
function Footer(): JSX.Element {
|
||||
const {
|
||||
@@ -120,7 +115,6 @@ function Footer(): JSX.Element {
|
||||
testAlertRule,
|
||||
]);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const handleSaveAlert = useCallback((): void => {
|
||||
const payload = buildCreateThresholdAlertRulePayload({
|
||||
alertType,
|
||||
@@ -139,9 +133,6 @@ function Footer(): JSX.Element {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
void invalidateGetRuleByID(queryClient, { id: ruleId });
|
||||
void invalidateListRules(queryClient);
|
||||
|
||||
toast.success('Alert rule updated successfully');
|
||||
safeNavigate('/alerts');
|
||||
},
|
||||
|
||||
@@ -7,7 +7,6 @@ import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationS
|
||||
|
||||
import * as createAlertState from '../../context';
|
||||
import Footer from '../Footer';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
// Mock the hooks used by Footer component
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
@@ -65,12 +64,6 @@ const mockAlertContextState = createMockAlertContextState({
|
||||
},
|
||||
});
|
||||
|
||||
const WrappedFooter = (): JSX.Element => (
|
||||
<MockQueryClientProvider>
|
||||
<Footer />
|
||||
</MockQueryClientProvider>
|
||||
);
|
||||
|
||||
jest
|
||||
.spyOn(createAlertState, 'useCreateAlertState')
|
||||
.mockReturnValue(mockAlertContextState);
|
||||
@@ -104,20 +97,20 @@ describe('Footer', () => {
|
||||
});
|
||||
|
||||
it('should render the component with 3 buttons', () => {
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
expect(screen.getByText(SAVE_ALERT_RULE_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(TEST_NOTIFICATION_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(DISCARD_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('discard action works correctly', () => {
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(DISCARD_TEXT));
|
||||
expect(mockDiscardAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('save alert rule action works correctly', () => {
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(SAVE_ALERT_RULE_TEXT));
|
||||
expect(mockCreateAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
@@ -127,13 +120,13 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isEditMode: true,
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(SAVE_ALERT_RULE_TEXT));
|
||||
expect(mockUpdateAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('test notification action works correctly', () => {
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(TEST_NOTIFICATION_TEXT));
|
||||
expect(mockTestAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
@@ -143,7 +136,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isCreatingAlertRule: true,
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -159,7 +152,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isUpdatingAlertRule: true,
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
// Target the button elements directly instead of the text spans inside them
|
||||
expect(
|
||||
@@ -176,7 +169,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isTestingAlertRule: true,
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
// Target the button elements directly instead of the text spans inside them
|
||||
expect(
|
||||
@@ -196,7 +189,7 @@ describe('Footer', () => {
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -224,7 +217,7 @@ describe('Footer', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -252,7 +245,7 @@ describe('Footer', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -268,7 +261,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isTestingAlertRule: true,
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
// When testing alert rule, the play icon is replaced with a loader icon
|
||||
expect(
|
||||
@@ -283,7 +276,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isUpdatingAlertRule: true,
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
// When updating alert rule, the check icon is replaced with a loader icon
|
||||
expect(
|
||||
@@ -298,7 +291,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isCreatingAlertRule: true,
|
||||
});
|
||||
render(<WrappedFooter />);
|
||||
render(<Footer />);
|
||||
|
||||
// When creating alert rule, the check icon is replaced with a loader icon
|
||||
expect(
|
||||
|
||||
@@ -186,40 +186,77 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
section {
|
||||
.section-1 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: unset;
|
||||
padding: 8px;
|
||||
height: 20px;
|
||||
padding: 16px 18px 18px 14px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 6px;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.section-2 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
padding: 16px 18px 18px 14px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
border-top: none;
|
||||
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.delete-dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
|
||||
.section-1,
|
||||
.section-2 {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.delete-dashboard .ant-btn {
|
||||
color: var(--bg-cherry-400) !important;
|
||||
.ant-typography {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
padding: 16px 18px 18px 14px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--bg-cherry-400) !important;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,12 +211,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
|
||||
.typography-variables {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.default-value-description {
|
||||
display: block;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SyncTooltipFilterMode,
|
||||
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { Check, ExternalLink, SolidInfoCircle, X } from '@signozhq/icons';
|
||||
import { Check, ExternalLink, Info, X } from '@signozhq/icons';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
|
||||
import styles from './GeneralSettings.module.scss';
|
||||
@@ -201,7 +201,7 @@ function GeneralDashboardSettings(): JSX.Element {
|
||||
placement="top"
|
||||
mouseEnterDelay={0.5}
|
||||
>
|
||||
<SolidInfoCircle size="md" className={styles.crossPanelSyncInfoIcon} />
|
||||
<Info size={14} className={styles.crossPanelSyncInfoIcon} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className={styles.crossPanelSyncRow}>
|
||||
|
||||
@@ -38,7 +38,6 @@ import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/map
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import Tabs2 from 'periscope/components/Tabs2';
|
||||
import { useAlertRuleOptional } from 'providers/Alert';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -93,6 +92,7 @@ const ALERT_SETUP_GUIDE_URLS: Record<AlertTypes, string> = {
|
||||
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function FormAlertRules({
|
||||
alertType,
|
||||
formInstance,
|
||||
@@ -160,32 +160,6 @@ function FormAlertRules({
|
||||
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
|
||||
const [yAxisUnit, setYAxisUnit] = useState<string>(currentQuery.unit || '');
|
||||
|
||||
const alertRuleContext = useAlertRuleOptional();
|
||||
const providerAlertName = alertRuleContext?.alertRuleName;
|
||||
useEffect(() => {
|
||||
if (providerAlertName) {
|
||||
setAlertDef((prev) => {
|
||||
if (prev.alert === providerAlertName) {
|
||||
return prev;
|
||||
}
|
||||
return { ...prev, alert: providerAlertName };
|
||||
});
|
||||
formInstance.setFieldsValue({ alert: providerAlertName });
|
||||
}
|
||||
}, [providerAlertName, formInstance]);
|
||||
|
||||
// Wrap setAlertDef to sync alert name to provider when user types
|
||||
const handleSetAlertDef = useCallback(
|
||||
(newDef: AlertDef) => {
|
||||
setAlertDef(newDef);
|
||||
// Sync alert name change to provider for header display
|
||||
if (newDef.alert !== alertDef.alert && alertRuleContext?.setAlertRuleName) {
|
||||
alertRuleContext.setAlertRuleName(newDef.alert);
|
||||
}
|
||||
},
|
||||
[alertDef.alert, alertRuleContext],
|
||||
);
|
||||
|
||||
const alertTypeFromURL = urlQuery.get(QueryParams.ruleType);
|
||||
|
||||
const [detectionMethod, setDetectionMethod] = useState<string | null>(null);
|
||||
@@ -706,7 +680,7 @@ function FormAlertRules({
|
||||
const renderBasicInfo = (): JSX.Element => (
|
||||
<BasicInfo
|
||||
alertDef={alertDef}
|
||||
setAlertDef={handleSetAlertDef}
|
||||
setAlertDef={setAlertDef}
|
||||
isNewRule={isNewRule}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -26,13 +26,10 @@
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.widget-header-title {
|
||||
max-width: 80%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.widget-header-actions {
|
||||
|
||||
@@ -438,7 +438,9 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
data: {
|
||||
name: values.name,
|
||||
tags: updatedTags,
|
||||
expires_at: dayjs(values.expires_at).endOf('day').toISOString(),
|
||||
expires_at: new Date(
|
||||
dayjs(values.expires_at).endOf('day').toISOString(),
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -469,11 +471,13 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
const requestPayload = {
|
||||
name: values.name,
|
||||
tags: updatedTags,
|
||||
expires_at: dayjs(values.expires_at).endOf('day').toISOString(),
|
||||
expires_at: new Date(dayjs(values.expires_at).endOf('day').toISOString()),
|
||||
};
|
||||
|
||||
createIngestionKey(
|
||||
{ data: requestPayload },
|
||||
{
|
||||
data: requestPayload,
|
||||
},
|
||||
{
|
||||
onSuccess: (_data) => {
|
||||
notifications.success({
|
||||
|
||||
@@ -79,12 +79,12 @@ describe('MultiIngestionSettings Page', () => {
|
||||
keys: [
|
||||
{
|
||||
name: 'Key One',
|
||||
expires_at: TEST_EXPIRES_AT,
|
||||
expires_at: new Date(TEST_EXPIRES_AT),
|
||||
value: 'secret',
|
||||
workspace_id: TEST_WORKSPACE_ID,
|
||||
id: 'k1',
|
||||
created_at: TEST_CREATED_UPDATED,
|
||||
updated_at: TEST_CREATED_UPDATED,
|
||||
created_at: new Date(TEST_CREATED_UPDATED),
|
||||
updated_at: new Date(TEST_CREATED_UPDATED),
|
||||
tags: [],
|
||||
limits: [
|
||||
{
|
||||
@@ -160,12 +160,12 @@ describe('MultiIngestionSettings Page', () => {
|
||||
keys: [
|
||||
{
|
||||
name: 'Key Logs',
|
||||
expires_at: TEST_EXPIRES_AT,
|
||||
expires_at: new Date(TEST_EXPIRES_AT),
|
||||
value: 'secret',
|
||||
workspace_id: TEST_WORKSPACE_ID,
|
||||
id: 'k2',
|
||||
created_at: TEST_CREATED_UPDATED,
|
||||
updated_at: TEST_CREATED_UPDATED,
|
||||
created_at: new Date(TEST_CREATED_UPDATED),
|
||||
updated_at: new Date(TEST_CREATED_UPDATED),
|
||||
tags: [],
|
||||
limits: [
|
||||
{
|
||||
@@ -238,12 +238,12 @@ describe('MultiIngestionSettings Page', () => {
|
||||
keys: [
|
||||
{
|
||||
name: KEY_NAME,
|
||||
expires_at: TEST_EXPIRES_AT,
|
||||
expires_at: new Date(TEST_EXPIRES_AT),
|
||||
value: 'secret',
|
||||
workspace_id: TEST_WORKSPACE_ID,
|
||||
id: 'k1',
|
||||
created_at: TEST_CREATED_UPDATED,
|
||||
updated_at: TEST_CREATED_UPDATED,
|
||||
created_at: new Date(TEST_CREATED_UPDATED),
|
||||
updated_at: new Date(TEST_CREATED_UPDATED),
|
||||
tags: [],
|
||||
limits: [
|
||||
{
|
||||
@@ -299,12 +299,12 @@ describe('MultiIngestionSettings Page', () => {
|
||||
keys: [
|
||||
{
|
||||
name: 'Key Regular',
|
||||
expires_at: TEST_EXPIRES_AT,
|
||||
expires_at: new Date(TEST_EXPIRES_AT),
|
||||
value: 'secret1',
|
||||
workspace_id: TEST_WORKSPACE_ID,
|
||||
id: 'k1',
|
||||
created_at: TEST_CREATED_UPDATED,
|
||||
updated_at: TEST_CREATED_UPDATED,
|
||||
created_at: new Date(TEST_CREATED_UPDATED),
|
||||
updated_at: new Date(TEST_CREATED_UPDATED),
|
||||
tags: [],
|
||||
limits: [],
|
||||
},
|
||||
@@ -319,12 +319,12 @@ describe('MultiIngestionSettings Page', () => {
|
||||
keys: [
|
||||
{
|
||||
name: 'Key Search Result',
|
||||
expires_at: TEST_EXPIRES_AT,
|
||||
expires_at: new Date(TEST_EXPIRES_AT),
|
||||
value: 'secret2',
|
||||
workspace_id: TEST_WORKSPACE_ID,
|
||||
id: 'k2',
|
||||
created_at: TEST_CREATED_UPDATED,
|
||||
updated_at: TEST_CREATED_UPDATED,
|
||||
created_at: new Date(TEST_CREATED_UPDATED),
|
||||
updated_at: new Date(TEST_CREATED_UPDATED),
|
||||
tags: [],
|
||||
limits: [],
|
||||
},
|
||||
|
||||
@@ -111,7 +111,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
number: allAlertRules?.length,
|
||||
layout: 'new',
|
||||
});
|
||||
safeNavigate(ROUTES.ALERTS_NEW, {
|
||||
safeNavigate(ROUTES.ALERT_TYPE_SELECTION, {
|
||||
newTab: isModifierKeyPressed(e),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -13,9 +13,9 @@ describe('filterAlerts', () => {
|
||||
const mockAlertBase: Partial<RuletypesRuleDTO> = {
|
||||
state: 'active' as RuletypesAlertStateDTO,
|
||||
disabled: false,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
createdAt: new Date('2024-01-01T00:00:00Z'),
|
||||
createdBy: 'test-user',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: new Date('2024-01-01T00:00:00Z'),
|
||||
updatedBy: 'test-user',
|
||||
version: '1',
|
||||
condition: {
|
||||
|
||||
@@ -22,9 +22,3 @@
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// FieldRenderer is used inside log/trace/metric detail drawers (z-index 1000).
|
||||
// The design-system tooltip defaults to z-index 50 and would render behind them.
|
||||
.field-renderer-tooltip-content {
|
||||
--tooltip-z-index: 1000;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Divider } from 'antd';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Divider, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import { TagContainer, TagLabel, TagValue } from './FieldRenderer.styles';
|
||||
@@ -8,10 +7,6 @@ import { getFieldAttributes } from './utils';
|
||||
|
||||
import './FieldRenderer.styles.scss';
|
||||
|
||||
const TOOLTIP_CONTENT_PROPS = {
|
||||
className: 'field-renderer-tooltip-content',
|
||||
};
|
||||
|
||||
function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
|
||||
const { dataType, newField, logType } = getFieldAttributes(field);
|
||||
|
||||
@@ -19,16 +14,11 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
|
||||
<span className="field-renderer-container">
|
||||
{dataType && newField && logType ? (
|
||||
<>
|
||||
<TooltipSimple
|
||||
title={newField}
|
||||
side="left"
|
||||
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
|
||||
arrow
|
||||
>
|
||||
<Tooltip placement="left" title={newField} mouseLeaveDelay={0}>
|
||||
<Typography.Text truncate={1} className="label">
|
||||
{newField}{' '}
|
||||
</Typography.Text>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<div className="tags">
|
||||
<TagContainer>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { ActionItemProps } from './ActionItem';
|
||||
import TableView from './TableView';
|
||||
import { getBodyDisplayString, removeEscapeCharacters } from './utils';
|
||||
import { removeEscapeCharacters } from './utils';
|
||||
|
||||
import './Overview.styles.scss';
|
||||
|
||||
@@ -112,7 +112,7 @@ function Overview({
|
||||
children: (
|
||||
<div className="logs-body-content">
|
||||
<MEditor
|
||||
value={removeEscapeCharacters(getBodyDisplayString(logData.body))}
|
||||
value={removeEscapeCharacters(logData.body)}
|
||||
language="json"
|
||||
options={options}
|
||||
onChange={(): void => {}}
|
||||
|
||||
@@ -106,20 +106,10 @@ function TableView({
|
||||
isListViewPanel,
|
||||
]);
|
||||
|
||||
// When USE_JSON_BODY is enabled, body arrives as a pre-parsed object. Serialize it
|
||||
// back to a string so flattenObject keeps `body` as a single table row instead of
|
||||
// recursively expanding it into dotted sub-keys (body.message, body.foo.bar, …),
|
||||
// which would break the tree view in BodyContent that relies on record.field === 'body'.
|
||||
const flattenLogData: Record<string, string> | null = useMemo(() => {
|
||||
if (!logData) {
|
||||
return null;
|
||||
}
|
||||
const normalizedLog =
|
||||
typeof logData.body === 'object' && logData.body !== null
|
||||
? { ...logData, body: JSON.stringify(logData.body) }
|
||||
: logData;
|
||||
return flattenObject(normalizedLog);
|
||||
}, [logData]);
|
||||
const flattenLogData: Record<string, string> | null = useMemo(
|
||||
() => (logData ? flattenObject(logData) : null),
|
||||
[logData],
|
||||
);
|
||||
|
||||
const handleClick = (
|
||||
operator: string,
|
||||
|
||||
@@ -10,7 +10,7 @@ const MAX_BODY_BYTES = 100 * 1024; // 100 KB
|
||||
|
||||
// Hook for async JSON processing
|
||||
const useAsyncJSONProcessing = (
|
||||
value: string | Record<string, unknown>,
|
||||
value: string,
|
||||
shouldProcess: boolean,
|
||||
handleChangeSelectedView?: ChangeViewFunctionType,
|
||||
): {
|
||||
@@ -40,17 +40,11 @@ const useAsyncJSONProcessing = (
|
||||
return (): void => {};
|
||||
}
|
||||
|
||||
// When value is already a parsed object skip the size check and JSON parsing
|
||||
const parseBody = (): Record<string, unknown> | null => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
const byteSize = new Blob([value as string]).size;
|
||||
if (byteSize > MAX_BODY_BYTES) {
|
||||
return null;
|
||||
}
|
||||
return recursiveParseJSON(value as string);
|
||||
};
|
||||
// Avoid processing if the json is too large
|
||||
const byteSize = new Blob([value]).size;
|
||||
if (byteSize > MAX_BODY_BYTES) {
|
||||
return (): void => {};
|
||||
}
|
||||
|
||||
processingRef.current = true;
|
||||
setJsonState({ isLoading: true, treeData: null, error: null });
|
||||
@@ -59,8 +53,8 @@ const useAsyncJSONProcessing = (
|
||||
const processAsync = (): void => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const parsedBody = parseBody();
|
||||
if (parsedBody && !isEmpty(parsedBody)) {
|
||||
const parsedBody = recursiveParseJSON(value);
|
||||
if (!isEmpty(parsedBody)) {
|
||||
const treeData = jsonToDataNodes(parsedBody, {
|
||||
isBodyJsonQueryEnabled,
|
||||
handleChangeSelectedView,
|
||||
@@ -88,8 +82,8 @@ const useAsyncJSONProcessing = (
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
(): void => {
|
||||
try {
|
||||
const parsedBody = parseBody();
|
||||
if (parsedBody && !isEmpty(parsedBody)) {
|
||||
const parsedBody = recursiveParseJSON(value);
|
||||
if (!isEmpty(parsedBody)) {
|
||||
const treeData = jsonToDataNodes(parsedBody, {
|
||||
isBodyJsonQueryEnabled,
|
||||
handleChangeSelectedView,
|
||||
|
||||
@@ -4,11 +4,7 @@ import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import dompurify from 'dompurify';
|
||||
import { uniqueId } from 'lodash-es';
|
||||
import {
|
||||
ILog,
|
||||
ILogAggregateAttributesResources,
|
||||
ILogBody,
|
||||
} from 'types/api/logs/log';
|
||||
import { ILog, ILogAggregateAttributesResources } from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { FORBID_DOM_PURIFY_ATTR, FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||
|
||||
@@ -437,8 +433,3 @@ export const getSanitizedLogBody = (
|
||||
return '{}';
|
||||
}
|
||||
};
|
||||
|
||||
// Returns a plain string for display contexts (Monaco editor, table cells, raw log row).
|
||||
export function getBodyDisplayString(body: string | ILogBody): string {
|
||||
return typeof body === 'string' ? body : JSON.stringify(body as ILogBody);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const mockUsers: TypesUserDTO[] = [
|
||||
displayName: 'Alice Smith',
|
||||
email: 'alice@signoz.io',
|
||||
status: 'active',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
createdAt: new Date('2024-01-01T00:00:00.000Z'),
|
||||
orgId: 'org-1',
|
||||
},
|
||||
{
|
||||
@@ -28,7 +28,7 @@ const mockUsers: TypesUserDTO[] = [
|
||||
displayName: 'Bob Jones',
|
||||
email: 'bob@signoz.io',
|
||||
status: 'active',
|
||||
createdAt: '2024-01-02T00:00:00.000Z',
|
||||
createdAt: new Date('2024-01-02T00:00:00.000Z'),
|
||||
orgId: 'org-1',
|
||||
},
|
||||
{
|
||||
@@ -36,7 +36,7 @@ const mockUsers: TypesUserDTO[] = [
|
||||
displayName: '',
|
||||
email: 'charlie@signoz.io',
|
||||
status: 'pending_invite',
|
||||
createdAt: '2024-01-03T00:00:00.000Z',
|
||||
createdAt: new Date('2024-01-03T00:00:00.000Z'),
|
||||
orgId: 'org-1',
|
||||
},
|
||||
{
|
||||
@@ -44,7 +44,7 @@ const mockUsers: TypesUserDTO[] = [
|
||||
displayName: 'Dave Deleted',
|
||||
email: 'dave@signoz.io',
|
||||
status: 'deleted',
|
||||
createdAt: '2024-01-04T00:00:00.000Z',
|
||||
createdAt: new Date('2024-01-04T00:00:00.000Z'),
|
||||
orgId: 'org-1',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Spin } from 'antd';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Button, Spin, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { useGetMetricHighlights } from 'api/generated/services/metrics';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { Info } from '@signozhq/icons';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
|
||||
import { HighlightsProps } from './types';
|
||||
@@ -14,10 +11,6 @@ import {
|
||||
formatTimestampToReadableDate,
|
||||
} from './utils';
|
||||
|
||||
const TOOLTIP_CONTENT_PROPS = {
|
||||
className: 'metric-highlights-tooltip-content',
|
||||
};
|
||||
|
||||
function Highlights({ metricName }: HighlightsProps): JSX.Element {
|
||||
const {
|
||||
data: metricHighlightsData,
|
||||
@@ -46,13 +39,6 @@ function Highlights({ metricName }: HighlightsProps): JSX.Element {
|
||||
const lastReceivedText = formatTimestampToReadableDate(
|
||||
metricHighlights?.lastReceived,
|
||||
);
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
const lastReceivedTooltipText = metricHighlights?.lastReceived
|
||||
? `Last received on ${formatTimezoneAdjustedTimestamp(
|
||||
metricHighlights.lastReceived,
|
||||
DATE_TIME_FORMATS.DASH_DATETIME_UTC,
|
||||
)}`
|
||||
: 'No data received yet';
|
||||
|
||||
if (isErrorMetricHighlights) {
|
||||
return (
|
||||
@@ -104,42 +90,27 @@ function Highlights({ metricName }: HighlightsProps): JSX.Element {
|
||||
className="metric-details-grid-value"
|
||||
data-testid="metric-highlights-data-points"
|
||||
>
|
||||
<TooltipSimple
|
||||
title={metricHighlights?.dataPoints?.toLocaleString()}
|
||||
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
|
||||
arrow
|
||||
>
|
||||
<span>
|
||||
{formatNumberIntoHumanReadableFormat(
|
||||
metricHighlights?.dataPoints ?? 0,
|
||||
)}
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
<Tooltip title={metricHighlights?.dataPoints?.toLocaleString()}>
|
||||
{formatNumberIntoHumanReadableFormat(metricHighlights?.dataPoints ?? 0)}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
className="metric-details-grid-value"
|
||||
data-testid="metric-highlights-time-series-total"
|
||||
>
|
||||
<TooltipSimple
|
||||
title="Active time series are those that have received data points in the last 1 hour."
|
||||
side="top"
|
||||
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
|
||||
arrow
|
||||
<Tooltip
|
||||
title="Active time series are those that have received data points in the last 1
|
||||
hour."
|
||||
placement="top"
|
||||
>
|
||||
<span>{`${timeSeriesTotal} total ⎯ ${timeSeriesActive} active`}</span>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
className="metric-details-grid-value"
|
||||
data-testid="metric-highlights-last-received"
|
||||
>
|
||||
<TooltipSimple
|
||||
title={lastReceivedTooltipText}
|
||||
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
|
||||
arrow
|
||||
>
|
||||
<span>{lastReceivedText}</span>
|
||||
</TooltipSimple>
|
||||
<Tooltip title={lastReceivedText}>{lastReceivedText}</Tooltip>
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -510,12 +510,6 @@
|
||||
color: var(--bg-robin-400) !important;
|
||||
}
|
||||
|
||||
// The MetricDetails Drawer sits at z-index 1000; the design-system tooltip
|
||||
// defaults to z-index 50 and would otherwise render behind the drawer.
|
||||
.metric-highlights-tooltip-content {
|
||||
--tooltip-z-index: 1000;
|
||||
}
|
||||
|
||||
@keyframes fade-in-out {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
||||
import TimezoneProvider from 'providers/Timezone';
|
||||
|
||||
import Highlights from '../Highlights';
|
||||
import { formatTimestampToReadableDate } from '../utils';
|
||||
import { getMockMetricHighlightsData, MOCK_METRIC_NAME } from './testUtlls';
|
||||
|
||||
function renderHighlights(metricName: string): ReturnType<typeof render> {
|
||||
return render(
|
||||
<TimezoneProvider>
|
||||
<TooltipProvider>
|
||||
<Highlights metricName={metricName} />
|
||||
</TooltipProvider>
|
||||
</TimezoneProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
const useGetMetricHighlightsMock = jest.spyOn(
|
||||
metricsExplorerHooks,
|
||||
'useGetMetricHighlights',
|
||||
@@ -28,7 +16,7 @@ describe('Highlights', () => {
|
||||
});
|
||||
|
||||
it('should render all highlights data correctly', () => {
|
||||
renderHighlights(MOCK_METRIC_NAME);
|
||||
render(<Highlights metricName={MOCK_METRIC_NAME} />);
|
||||
|
||||
const dataPoints = screen.getByTestId('metric-highlights-data-points');
|
||||
const timeSeriesTotal = screen.getByTestId(
|
||||
@@ -53,7 +41,7 @@ describe('Highlights', () => {
|
||||
),
|
||||
);
|
||||
|
||||
renderHighlights(MOCK_METRIC_NAME);
|
||||
render(<Highlights metricName={MOCK_METRIC_NAME} />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId('metric-highlights-error-state'),
|
||||
@@ -70,7 +58,7 @@ describe('Highlights', () => {
|
||||
),
|
||||
);
|
||||
|
||||
renderHighlights(MOCK_METRIC_NAME);
|
||||
render(<Highlights metricName={MOCK_METRIC_NAME} />);
|
||||
|
||||
expect(screen.getByText('SAMPLES')).toBeInTheDocument();
|
||||
expect(screen.getByText('TIME SERIES')).toBeInTheDocument();
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
.new-explorer-cta-with-badge {
|
||||
display: inline-flex;
|
||||
.new-explorer-cta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--muted-foreground);
|
||||
|
||||
/* Bifrost (Ancient)/Content/sm */
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
export const RIBBON_STYLES = {
|
||||
top: '-0.75rem',
|
||||
};
|
||||
|
||||
export const buttonText: Record<string, string> = {
|
||||
[ROUTES.LOGS_EXPLORER]: 'Old Explorer',
|
||||
[ROUTES.TRACE]: 'New Explorer',
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Button } from 'antd';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Badge, Button } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { Undo } from '@signozhq/icons';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import { buttonText } from './config';
|
||||
import { buttonText, RIBBON_STYLES } from './config';
|
||||
|
||||
import './NewExplorerCTA.styles.scss';
|
||||
|
||||
@@ -71,12 +70,9 @@ function NewExplorerCTA(): JSX.Element | null {
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="new-explorer-cta-with-badge">
|
||||
<Badge.Ribbon style={RIBBON_STYLES} text="New">
|
||||
{button}
|
||||
<Badge color="robin" variant="default">
|
||||
New
|
||||
</Badge>
|
||||
</span>
|
||||
</Badge.Ribbon>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { SolidAlertTriangle } from '@signozhq/icons';
|
||||
import { ConfirmDialog } from '@signozhq/ui/dialog';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
export interface DiscardChangesModalProps {
|
||||
open: boolean;
|
||||
isNewPanel: boolean;
|
||||
panelTitle?: string;
|
||||
dashboardTitle?: string;
|
||||
onDiscard: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function DiscardChangesModal({
|
||||
open,
|
||||
isNewPanel,
|
||||
panelTitle,
|
||||
dashboardTitle,
|
||||
onDiscard,
|
||||
onClose,
|
||||
}: DiscardChangesModalProps): JSX.Element {
|
||||
const dashboardName = dashboardTitle ? (
|
||||
<>
|
||||
{' '}
|
||||
to <strong>{dashboardTitle}</strong>
|
||||
</>
|
||||
) : null;
|
||||
const panelLabel = panelTitle ? <strong>{panelTitle}</strong> : 'this panel';
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
open={open}
|
||||
onOpenChange={(next): void => {
|
||||
if (!next) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title="Discard changes?"
|
||||
titleIcon={<SolidAlertTriangle size={14} color="#fdd600" />}
|
||||
confirmText="Discard"
|
||||
confirmColor="destructive"
|
||||
cancelText="Keep editing"
|
||||
onConfirm={onDiscard}
|
||||
onCancel={onClose}
|
||||
>
|
||||
{isNewPanel ? (
|
||||
<Typography>This new panel won't be added{dashboardName}.</Typography>
|
||||
) : (
|
||||
<Typography>Your unsaved edits to {panelLabel} will be lost.</Typography>
|
||||
)}
|
||||
</ConfirmDialog>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
import {
|
||||
initialAutocompleteData,
|
||||
initialQueryBuilderFormValuesMap,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { MetricAggregation } from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import type { PartialPanelTypes } from '../utils';
|
||||
import { getIsQueryModified, handleQueryChange } from '../utils';
|
||||
import { handleQueryChange } from '../utils';
|
||||
|
||||
const buildSupersetQuery = (extras?: Record<string, unknown>): Query => ({
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
@@ -41,128 +37,6 @@ const buildSupersetQuery = (extras?: Record<string, unknown>): Query => ({
|
||||
},
|
||||
});
|
||||
|
||||
const buildMetricsQuery = (
|
||||
overrides?: Partial<{
|
||||
metricName: string;
|
||||
aggregateAttributeKey: string;
|
||||
legend: string;
|
||||
groupByKey: string;
|
||||
}>,
|
||||
): Query => ({
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
id: 'query-id',
|
||||
unit: '',
|
||||
builder: {
|
||||
queryFormulas: [],
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap[DataSource.METRICS],
|
||||
queryName: 'A',
|
||||
aggregateAttribute: overrides?.aggregateAttributeKey
|
||||
? {
|
||||
...initialAutocompleteData,
|
||||
key: overrides.aggregateAttributeKey,
|
||||
type: 'tag',
|
||||
dataType: DataTypes.Float64,
|
||||
}
|
||||
: cloneDeep(initialAutocompleteData),
|
||||
aggregations: [
|
||||
{
|
||||
metricName: overrides?.metricName ?? 'system.cpu.load',
|
||||
temporality: '',
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
reduceTo: 'avg',
|
||||
} as MetricAggregation,
|
||||
],
|
||||
legend: overrides?.legend ?? '',
|
||||
groupBy: overrides?.groupByKey
|
||||
? [
|
||||
{
|
||||
...initialAutocompleteData,
|
||||
key: overrides.groupByKey,
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
});
|
||||
|
||||
describe('getIsQueryModified', () => {
|
||||
it('returns false when baseline is null (new unsaved panel with no edits anchor)', () => {
|
||||
const current = buildMetricsQuery();
|
||||
expect(getIsQueryModified(current, null)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when baseline is undefined', () => {
|
||||
const current = buildMetricsQuery();
|
||||
expect(getIsQueryModified(current, undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when current only differs by auto-backfilled aggregateAttribute', () => {
|
||||
// saved widget query: aggregateAttribute is the v5-style empty initial value
|
||||
// (stripped from persisted spec; spread back in as initialAutocompleteData on load)
|
||||
const savedQuery = buildMetricsQuery({ metricName: 'system.cpu.load' });
|
||||
// after MetricNameSelector edit-mode backfill, currentQuery has the populated
|
||||
// aggregateAttribute while the rest of the query is identical
|
||||
const currentQuery = buildMetricsQuery({
|
||||
metricName: 'system.cpu.load',
|
||||
aggregateAttributeKey: 'system.cpu.load',
|
||||
});
|
||||
expect(getIsQueryModified(currentQuery, savedQuery)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when the user edits the legend', () => {
|
||||
const baseline = buildMetricsQuery({ metricName: 'system.cpu.load' });
|
||||
const edited = buildMetricsQuery({
|
||||
metricName: 'system.cpu.load',
|
||||
legend: 'cpu-load',
|
||||
});
|
||||
expect(getIsQueryModified(edited, baseline)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when the user picks a different metric (aggregations diverges)', () => {
|
||||
const baseline = buildMetricsQuery({ metricName: 'system.cpu.load' });
|
||||
const edited = buildMetricsQuery({ metricName: 'system.memory.usage' });
|
||||
expect(getIsQueryModified(edited, baseline)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when the user adds a groupBy', () => {
|
||||
const baseline = buildMetricsQuery({ metricName: 'system.cpu.load' });
|
||||
const edited = buildMetricsQuery({
|
||||
metricName: 'system.cpu.load',
|
||||
groupByKey: 'host.name',
|
||||
});
|
||||
expect(getIsQueryModified(edited, baseline)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true on existing widget when current diverges from saved (Stage-and-Run silent-loss flow)', () => {
|
||||
// After Edit → Stage and Run, stagedQuery is reset to match currentQuery.
|
||||
// The dirty check must compare against the SAVED widget query, not stagedQuery.
|
||||
const savedQuery = buildMetricsQuery({ metricName: 'system.cpu.load' });
|
||||
const currentQuery = buildMetricsQuery({ metricName: 'system.memory.usage' });
|
||||
expect(getIsQueryModified(currentQuery, savedQuery)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for a new panel where currentQuery still matches stagedQuery baseline', () => {
|
||||
const stagedQuery = buildMetricsQuery();
|
||||
const currentQuery = buildMetricsQuery();
|
||||
expect(getIsQueryModified(currentQuery, stagedQuery)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for a new panel where currentQuery has been edited away from stagedQuery', () => {
|
||||
const stagedQuery = buildMetricsQuery();
|
||||
const currentQuery = buildMetricsQuery({ legend: 'custom' });
|
||||
expect(getIsQueryModified(currentQuery, stagedQuery)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleQueryChange', () => {
|
||||
it('sets list-specific fields when switching to LIST', () => {
|
||||
const superset = buildSupersetQuery();
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { Check, X } from '@signozhq/icons';
|
||||
import { Check, SolidAlertTriangle, X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from '@signozhq/ui/resizable';
|
||||
import { Flex } from 'antd';
|
||||
import { Flex, Modal, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
||||
@@ -68,6 +69,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getGraphType, getGraphTypeForFormat } from 'utils/getGraphType';
|
||||
|
||||
import LeftContainer from './LeftContainer';
|
||||
import QueryTypeTag from './LeftContainer/QueryTypeTag';
|
||||
import RightContainer from './RightContainer';
|
||||
import { ThresholdProps } from './RightContainer/Threshold/types';
|
||||
import TimeItems, { timePreferance } from './RightContainer/timeItems';
|
||||
@@ -80,7 +82,6 @@ import {
|
||||
placeWidgetAtBottom,
|
||||
placeWidgetBetweenRows,
|
||||
} from './utils';
|
||||
import DiscardChangesModal from './WidgetModals/DiscardChangesModal';
|
||||
|
||||
import './NewWidget.styles.scss';
|
||||
|
||||
@@ -97,6 +98,8 @@ function NewWidget({
|
||||
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
|
||||
const {
|
||||
@@ -107,6 +110,11 @@ function NewWidget({
|
||||
setSupersetQuery,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const isQueryModified = useMemo(
|
||||
() => getIsQueryModified(currentQuery, stagedQuery),
|
||||
[currentQuery, stagedQuery],
|
||||
);
|
||||
|
||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
@@ -131,23 +139,6 @@ function NewWidget({
|
||||
|
||||
const query = useUrlQuery();
|
||||
|
||||
// For existing widgets, compare currentQuery against the saved widget query
|
||||
// (stable across Stage-and-Run cycles). For new panels with no saved baseline,
|
||||
// fall back to stagedQuery so initial edits still trigger the warning.
|
||||
const savedWidgetQuery = useMemo(() => {
|
||||
const widgetId = query.get('widgetId');
|
||||
const match = widgets?.find((w) => w.id === widgetId);
|
||||
if (!match || match.panelTypes === PANEL_GROUP_TYPES.ROW) {
|
||||
return null;
|
||||
}
|
||||
return (match as Widgets).query ?? null;
|
||||
}, [widgets, query]);
|
||||
|
||||
const isQueryModified = useMemo(
|
||||
() => getIsQueryModified(currentQuery, savedWidgetQuery ?? stagedQuery),
|
||||
[currentQuery, savedWidgetQuery, stagedQuery],
|
||||
);
|
||||
|
||||
const [isNewDashboard, setIsNewDashboard] = useState<boolean>(false);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
@@ -237,6 +228,7 @@ function NewWidget({
|
||||
Record<string, string>
|
||||
>(selectedWidget?.customLegendColors || {});
|
||||
|
||||
const [saveModal, setSaveModal] = useState(false);
|
||||
const [discardModal, setDiscardModal] = useState(false);
|
||||
|
||||
const [bucketWidth, setBucketWidth] = useState<number>(
|
||||
@@ -348,6 +340,7 @@ function NewWidget({
|
||||
]);
|
||||
|
||||
const closeModal = (): void => {
|
||||
setSaveModal(false);
|
||||
setDiscardModal(false);
|
||||
};
|
||||
|
||||
@@ -600,7 +593,7 @@ function NewWidget({
|
||||
},
|
||||
};
|
||||
|
||||
return updateDashboardMutation.mutateAsync(dashboard, {
|
||||
updateDashboardMutation.mutateAsync(dashboard, {
|
||||
onSuccess: () => {
|
||||
setToScrollWidgetId(selectedWidget?.id || '');
|
||||
navigateToDashboardPage();
|
||||
@@ -695,9 +688,9 @@ function NewWidget({
|
||||
})),
|
||||
}),
|
||||
});
|
||||
onClickSaveHandler();
|
||||
setSaveModal(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onClickSaveHandler]);
|
||||
}, [isNewPanel]);
|
||||
|
||||
const isNewTraceLogsAvailable =
|
||||
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
||||
@@ -958,14 +951,57 @@ function NewWidget({
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</PanelContainer>
|
||||
<DiscardChangesModal
|
||||
<Modal
|
||||
title={
|
||||
isQueryModified ? (
|
||||
<Space>
|
||||
<SolidAlertTriangle size={16} color="#fdd600" />
|
||||
Unsaved Changes
|
||||
</Space>
|
||||
) : (
|
||||
'Save Widget'
|
||||
)
|
||||
}
|
||||
focusTriggerAfterClose
|
||||
forceRender
|
||||
destroyOnClose
|
||||
closable
|
||||
onCancel={closeModal}
|
||||
onOk={onClickSaveHandler}
|
||||
confirmLoading={updateDashboardMutation.isLoading}
|
||||
centered
|
||||
open={saveModal}
|
||||
width={600}
|
||||
>
|
||||
{!isQueryModified ? (
|
||||
<Typography>
|
||||
{t('your_graph_build_with')}{' '}
|
||||
<QueryTypeTag queryType={currentQuery.queryType} />{' '}
|
||||
{t('dashboard_ok_confirm')}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography>{t('dashboard_unsave_changes')} </Typography>
|
||||
)}
|
||||
</Modal>
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<SolidAlertTriangle size={16} color="#fdd600" />
|
||||
Unsaved Changes
|
||||
</Space>
|
||||
}
|
||||
focusTriggerAfterClose
|
||||
forceRender
|
||||
destroyOnClose
|
||||
closable
|
||||
onCancel={closeModal}
|
||||
onOk={discardChanges}
|
||||
centered
|
||||
open={discardModal}
|
||||
isNewPanel={isNewPanel}
|
||||
panelTitle={title}
|
||||
dashboardTitle={dashboardData?.data?.title}
|
||||
onDiscard={discardChanges}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
width={600}
|
||||
>
|
||||
<Typography>{t('dashboard_unsave_changes')}</Typography>
|
||||
</Modal>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
|
||||
import { PrecisionOptionsEnum } from 'components/Graph/types';
|
||||
import { YAxisCategoryNames } from 'components/YAxisUnitSelector/constants';
|
||||
import {
|
||||
@@ -25,84 +26,16 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { getCategoryName } from './RightContainer/dataFormatCategories';
|
||||
|
||||
// Asks "would saving the current panel change the persisted widget spec?".
|
||||
//
|
||||
// `adjustQueryForV5` is deliberately not reused here: in addition to stripping
|
||||
// the legacy v4 fields, it also resurrects them onto each metric
|
||||
// `aggregations[i]`. That migration step is correct on save but bleeds
|
||||
// asymmetrically across a comparator — the live query still carries the
|
||||
// legacy defaults from `initialQueryBuilderFormValuesMap` while a previously
|
||||
// saved widget had them stripped.
|
||||
const stripQueryDataForCompare = (
|
||||
queryData: IBuilderQuery,
|
||||
): Record<string, unknown> => {
|
||||
const {
|
||||
aggregateAttribute: _aggregateAttribute,
|
||||
aggregateOperator: _aggregateOperator,
|
||||
timeAggregation: _timeAggregation,
|
||||
spaceAggregation: _spaceAggregation,
|
||||
reduceTo: _reduceTo,
|
||||
filters: _filters,
|
||||
...retained
|
||||
} = queryData ?? ({} as IBuilderQuery);
|
||||
|
||||
const groupBy = (retained.groupBy ?? []).map((entry) => {
|
||||
const { id: _id, ...rest } = entry;
|
||||
return rest;
|
||||
});
|
||||
|
||||
return {
|
||||
...retained,
|
||||
groupBy,
|
||||
source: retained.source || '',
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeForDirtyCheck = (query: Query): Record<string, unknown> => {
|
||||
const { id: _id, unit, builder, ...rest } = query;
|
||||
return {
|
||||
...rest,
|
||||
// `id` is regenerated on every Stage and Run; `unit` flips between ''
|
||||
// and undefined depending on whether the user has touched the selector.
|
||||
unit: unit || '',
|
||||
builder: {
|
||||
...builder,
|
||||
queryData: (builder?.queryData ?? []).map(stripQueryDataForCompare),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// `lodash.isEqual` distinguishes `{a: undefined}` from `{}`; for the dirty
|
||||
// check those are the same. Initial-values spreads on the live query
|
||||
// frequently leave such explicit-undefined keys.
|
||||
const stripUndefined = (value: unknown): unknown => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(stripUndefined);
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
const out: Record<string, unknown> = {};
|
||||
Object.entries(value as Record<string, unknown>).forEach(([k, v]) => {
|
||||
if (v === undefined) {
|
||||
return;
|
||||
}
|
||||
out[k] = stripUndefined(v);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export const getIsQueryModified = (
|
||||
currentQuery: Query,
|
||||
baselineQuery: Query | null | undefined,
|
||||
stagedQuery: Query | null,
|
||||
): boolean => {
|
||||
if (!baselineQuery) {
|
||||
if (!stagedQuery) {
|
||||
return false;
|
||||
}
|
||||
return !isEqual(
|
||||
stripUndefined(normalizeForDirtyCheck(baselineQuery)),
|
||||
stripUndefined(normalizeForDirtyCheck(currentQuery)),
|
||||
);
|
||||
const omitIdFromStageQuery = omitIdFromQuery(stagedQuery);
|
||||
const omitIdFromCurrentQuery = omitIdFromQuery(currentQuery);
|
||||
return !isEqual(omitIdFromStageQuery, omitIdFromCurrentQuery);
|
||||
};
|
||||
|
||||
export type PartialPanelTypes = {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Expand } from '@signozhq/icons';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { getBodyDisplayString } from 'container/LogDetailedView/utils';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
@@ -27,9 +26,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
|
||||
DATE_TIME_FORMATS.UTC_MONTH_SHORT,
|
||||
)}
|
||||
</div>
|
||||
<div className="logs-preview-list-item-body">
|
||||
{getBodyDisplayString(log.body)}
|
||||
</div>
|
||||
<div className="logs-preview-list-item-body">{log.body}</div>
|
||||
<div
|
||||
className="logs-preview-list-item-expand"
|
||||
onClick={makeLogDetailsHandler(log)}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
useListDowntimeSchedules,
|
||||
} from 'api/generated/services/downtimeschedules';
|
||||
import { useListRules } from 'api/generated/services/rules';
|
||||
import type { AlertmanagertypesPlannedMaintenanceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { RuletypesPlannedMaintenanceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import dayjs from 'dayjs';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -24,7 +24,7 @@ import { PlannedDowntimeDeleteModal } from './PlannedDowntimeDeleteModal';
|
||||
import { PlannedDowntimeForm } from './PlannedDowntimeForm';
|
||||
import { PlannedDowntimeList } from './PlannedDowntimeList';
|
||||
import {
|
||||
defaultInitialValues,
|
||||
defautlInitialValues,
|
||||
deleteDowntimeHandler,
|
||||
} from './PlannedDowntimeutils';
|
||||
|
||||
@@ -48,8 +48,8 @@ export function PlannedDowntime(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const [initialValues, setInitialValues] =
|
||||
useState<Partial<AlertmanagertypesPlannedMaintenanceDTO>>(
|
||||
defaultInitialValues,
|
||||
useState<Partial<RuletypesPlannedMaintenanceDTO & { editMode: boolean }>>(
|
||||
defautlInitialValues,
|
||||
);
|
||||
|
||||
const downtimeSchedules = useListDowntimeSchedules();
|
||||
@@ -148,7 +148,7 @@ export function PlannedDowntime(): JSX.Element {
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={(): void => {
|
||||
setInitialValues(defaultInitialValues);
|
||||
setInitialValues({ ...defautlInitialValues, editMode: false });
|
||||
setIsOpen(true);
|
||||
setEditMode(false);
|
||||
form.resetFields();
|
||||
|
||||
@@ -20,9 +20,9 @@ import {
|
||||
updateDowntimeScheduleByID,
|
||||
} from 'api/generated/services/downtimeschedules';
|
||||
import type {
|
||||
AlertmanagertypesPlannedMaintenanceDTO,
|
||||
AlertmanagertypesPostablePlannedMaintenanceDTO,
|
||||
AlertmanagertypesRecurrenceDTO,
|
||||
RuletypesPlannedMaintenanceDTO,
|
||||
RuletypesPostablePlannedMaintenanceDTO,
|
||||
RuletypesRecurrenceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
@@ -46,6 +46,8 @@ import { AlertRuleTags } from './PlannedDowntimeList';
|
||||
import {
|
||||
getAlertOptionsFromIds,
|
||||
getDurationInfo,
|
||||
getEndTime,
|
||||
handleTimeConversion,
|
||||
isScheduleRecurring,
|
||||
recurrenceOptions,
|
||||
recurrenceOptionWithSubmenu,
|
||||
@@ -62,28 +64,24 @@ const TIME_FORMAT = DATE_TIME_FORMATS.TIME;
|
||||
const DATE_FORMAT = DATE_TIME_FORMATS.ORDINAL_DATE;
|
||||
const ORDINAL_FORMAT = DATE_TIME_FORMATS.ORDINAL_ONLY;
|
||||
|
||||
const TZ_OPTIONS: DefaultOptionType[] = ALL_TIME_ZONES.map(
|
||||
(timezone: string) => ({
|
||||
label: timezone,
|
||||
value: timezone,
|
||||
key: timezone,
|
||||
}),
|
||||
);
|
||||
|
||||
interface PlannedDowntimeFormData {
|
||||
name: string;
|
||||
startTime: dayjs.Dayjs | null;
|
||||
endTime: dayjs.Dayjs | null;
|
||||
recurrence?: AlertmanagertypesRecurrenceDTO;
|
||||
startTime: dayjs.Dayjs | string;
|
||||
endTime: dayjs.Dayjs | string;
|
||||
recurrence?: RuletypesRecurrenceDTO | null;
|
||||
alertRules: DefaultOptionType[];
|
||||
recurrenceSelect?: AlertmanagertypesRecurrenceDTO;
|
||||
recurrenceSelect?: RuletypesRecurrenceDTO;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
const customFormat = DATE_TIME_FORMATS.ORDINAL_DATETIME;
|
||||
|
||||
interface PlannedDowntimeFormProps {
|
||||
initialValues: Partial<AlertmanagertypesPlannedMaintenanceDTO>;
|
||||
initialValues: Partial<
|
||||
RuletypesPlannedMaintenanceDTO & {
|
||||
editMode: boolean;
|
||||
}
|
||||
>;
|
||||
alertOptions: DefaultOptionType[];
|
||||
isError: boolean;
|
||||
isLoading: boolean;
|
||||
@@ -91,7 +89,7 @@ interface PlannedDowntimeFormProps {
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
refetchAllSchedules: () => void;
|
||||
isEditMode: boolean;
|
||||
form: FormInstance;
|
||||
form: FormInstance<any>;
|
||||
}
|
||||
|
||||
export function PlannedDowntimeForm(
|
||||
@@ -109,46 +107,66 @@ export function PlannedDowntimeForm(
|
||||
form,
|
||||
} = props;
|
||||
|
||||
const [selectedTags, setSelectedTags] = React.useState<DefaultOptionType[]>(
|
||||
[],
|
||||
);
|
||||
const [selectedTags, setSelectedTags] = React.useState<
|
||||
DefaultOptionType | DefaultOptionType[]
|
||||
>([]);
|
||||
const alertRuleFormName = 'alertRules';
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const [durationUnit, setDurationUnit] = useState<string>(
|
||||
getDurationInfo(initialValues.schedule?.recurrence?.duration)?.unit || 'm',
|
||||
getDurationInfo(initialValues.schedule?.recurrence?.duration as string)
|
||||
?.unit || 'm',
|
||||
);
|
||||
|
||||
const [formData, setFormData] = useState<Partial<PlannedDowntimeFormData>>({
|
||||
timezone: initialValues.schedule?.timezone,
|
||||
});
|
||||
|
||||
const [recurrenceType, setRecurrenceType] = useState<string>(
|
||||
initialValues.schedule?.recurrence?.repeatType ||
|
||||
const [recurrenceType, setRecurrenceType] = useState<string | null>(
|
||||
(initialValues.schedule?.recurrence?.repeatType as string) ||
|
||||
recurrenceOptions.doesNotRepeat.value,
|
||||
);
|
||||
|
||||
const timezoneInitialValue = !isEmpty(initialValues.schedule?.timezone)
|
||||
? (initialValues.schedule?.timezone as string)
|
||||
: undefined;
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const requiredFieldRule = [{ required: true }];
|
||||
|
||||
const datePickerFooter = (mode: any): any =>
|
||||
mode === 'time' ? (
|
||||
<span style={{ color: 'gray' }}>Please select the time</span>
|
||||
) : null;
|
||||
|
||||
const saveHandler = useCallback(
|
||||
const saveHanlder = useCallback(
|
||||
async (values: PlannedDowntimeFormData) => {
|
||||
const data: AlertmanagertypesPostablePlannedMaintenanceDTO = {
|
||||
const shouldKeepLocalTime = !isEditMode;
|
||||
const data: RuletypesPostablePlannedMaintenanceDTO = {
|
||||
alertIds: values.alertRules
|
||||
.map((alert) => alert.value)
|
||||
.filter((alert) => alert !== undefined) as string[],
|
||||
name: values.name,
|
||||
schedule: {
|
||||
startTime: values.startTime?.format(),
|
||||
endTime: values.endTime?.format(),
|
||||
timezone: values.timezone!,
|
||||
recurrence: values.recurrence,
|
||||
startTime: new Date(
|
||||
handleTimeConversion(
|
||||
values.startTime,
|
||||
timezoneInitialValue,
|
||||
values.timezone,
|
||||
shouldKeepLocalTime,
|
||||
),
|
||||
),
|
||||
timezone: values.timezone as string,
|
||||
endTime: values.endTime
|
||||
? new Date(
|
||||
handleTimeConversion(
|
||||
values.endTime,
|
||||
timezoneInitialValue,
|
||||
values.timezone,
|
||||
shouldKeepLocalTime,
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
recurrence: values.recurrence as RuletypesRecurrenceDTO,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -180,58 +198,50 @@ export function PlannedDowntimeForm(
|
||||
notifications,
|
||||
refetchAllSchedules,
|
||||
setIsOpen,
|
||||
timezoneInitialValue,
|
||||
showErrorModal,
|
||||
],
|
||||
);
|
||||
const onFinish = async (values: PlannedDowntimeFormData): Promise<void> => {
|
||||
const { recurrence } = values;
|
||||
const recurrenceData =
|
||||
!recurrence ||
|
||||
recurrence.repeatType === recurrenceOptions.doesNotRepeat.value
|
||||
values?.recurrence?.repeatType === recurrenceOptions.doesNotRepeat.value
|
||||
? undefined
|
||||
: {
|
||||
duration: recurrence.duration
|
||||
? `${recurrence.duration}${durationUnit}`
|
||||
: '',
|
||||
startTime: values.startTime!.format(),
|
||||
endTime: values.endTime?.format(),
|
||||
repeatOn: recurrence.repeatOn,
|
||||
repeatType: recurrence.repeatType,
|
||||
duration: values.recurrence?.duration
|
||||
? `${values.recurrence?.duration}${durationUnit}`
|
||||
: undefined,
|
||||
endTime: !isEmpty(values.endTime)
|
||||
? handleTimeConversion(
|
||||
values.endTime,
|
||||
timezoneInitialValue,
|
||||
values.timezone,
|
||||
!isEditMode,
|
||||
)
|
||||
: undefined,
|
||||
startTime: handleTimeConversion(
|
||||
values.startTime,
|
||||
timezoneInitialValue,
|
||||
values.timezone,
|
||||
!isEditMode,
|
||||
),
|
||||
repeatOn: !values.recurrence?.repeatOn?.length
|
||||
? undefined
|
||||
: values.recurrence?.repeatOn,
|
||||
repeatType: values.recurrence?.repeatType,
|
||||
};
|
||||
|
||||
await saveHandler({
|
||||
const payloadValues = {
|
||||
...values,
|
||||
recurrence: recurrenceData,
|
||||
});
|
||||
recurrence: recurrenceData as RuletypesRecurrenceDTO | undefined,
|
||||
};
|
||||
await saveHanlder(payloadValues);
|
||||
};
|
||||
|
||||
const handleFormData = (data: Partial<PlannedDowntimeFormData>): void => {
|
||||
const { startTime, endTime, timezone } = data;
|
||||
const update: Partial<PlannedDowntimeFormData> = {};
|
||||
|
||||
// If the set timezone doesn't match, update it.
|
||||
if (
|
||||
startTime &&
|
||||
timezone &&
|
||||
startTime.format() !== startTime.tz(timezone, true).format()
|
||||
) {
|
||||
update.startTime = startTime.tz(timezone, true);
|
||||
}
|
||||
if (
|
||||
endTime &&
|
||||
timezone &&
|
||||
endTime.format() !== endTime.tz(timezone, true).format()
|
||||
) {
|
||||
update.endTime = endTime.tz(timezone, true);
|
||||
}
|
||||
|
||||
if (!isEmpty(update)) {
|
||||
data = { ...data, ...update };
|
||||
form.setFieldsValue({ ...update });
|
||||
}
|
||||
|
||||
setFormData(data);
|
||||
};
|
||||
const formValidationRules = [
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
const handleOk = async (): Promise<void> => {
|
||||
await form.validateFields().catch(() => {
|
||||
@@ -239,11 +249,16 @@ export function PlannedDowntimeForm(
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = (): void => setIsOpen(false);
|
||||
const handleCancel = (): void => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleAlertRulesChange: SelectProps['onChange'] = (_value, options) => {
|
||||
const handleChange = (
|
||||
_value: string,
|
||||
options: DefaultOptionType | DefaultOptionType[],
|
||||
): void => {
|
||||
form.setFieldValue(alertRuleFormName, options);
|
||||
setSelectedTags(Array.isArray(options) ? options : [options]);
|
||||
setSelectedTags(options);
|
||||
};
|
||||
|
||||
const noTagRenderer: SelectProps['tagRender'] = () => <></>;
|
||||
@@ -252,51 +267,113 @@ export function PlannedDowntimeForm(
|
||||
if (!removedTag) {
|
||||
return;
|
||||
}
|
||||
const newTags = selectedTags.filter((tag) => tag.value !== removedTag);
|
||||
const newTags = selectedTags.filter(
|
||||
(tag: DefaultOptionType) => tag.value !== removedTag,
|
||||
);
|
||||
form.setFieldValue(alertRuleFormName, newTags);
|
||||
setSelectedTags(newTags);
|
||||
};
|
||||
|
||||
const formattedInitialValues = useMemo((): PlannedDowntimeFormData => {
|
||||
const { schedule } = initialValues;
|
||||
const startTime = schedule?.recurrence?.startTime || schedule?.startTime;
|
||||
const endTime = schedule?.recurrence?.endTime || schedule?.endTime;
|
||||
|
||||
return {
|
||||
const formatedInitialValues = useMemo(() => {
|
||||
const formData: PlannedDowntimeFormData = {
|
||||
name: defaultTo(initialValues.name, ''),
|
||||
alertRules: getAlertOptionsFromIds(
|
||||
initialValues.alertIds || [],
|
||||
alertOptions,
|
||||
),
|
||||
startTime: startTime ? dayjs(startTime).tz(schedule.timezone) : null,
|
||||
endTime: endTime ? dayjs(endTime).tz(schedule.timezone) : null,
|
||||
endTime: getEndTime(initialValues) ? dayjs(getEndTime(initialValues)) : '',
|
||||
startTime: initialValues.schedule?.startTime
|
||||
? dayjs(initialValues.schedule?.startTime)
|
||||
: '',
|
||||
recurrence: {
|
||||
...schedule?.recurrence,
|
||||
repeatType: !isScheduleRecurring(schedule)
|
||||
...initialValues.schedule?.recurrence,
|
||||
repeatType: (!isScheduleRecurring(initialValues?.schedule)
|
||||
? recurrenceOptions.doesNotRepeat.value
|
||||
: schedule?.recurrence?.repeatType,
|
||||
duration: getDurationInfo(schedule?.recurrence?.duration)?.value ?? '',
|
||||
} as AlertmanagertypesRecurrenceDTO,
|
||||
timezone: schedule?.timezone as string,
|
||||
: initialValues.schedule?.recurrence
|
||||
?.repeatType) as RuletypesRecurrenceDTO['repeatType'],
|
||||
duration: String(
|
||||
getDurationInfo(initialValues.schedule?.recurrence?.duration as string)
|
||||
?.value ?? '',
|
||||
),
|
||||
} as RuletypesRecurrenceDTO,
|
||||
timezone: initialValues.schedule?.timezone as string,
|
||||
};
|
||||
return formData;
|
||||
}, [initialValues, alertOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTags(formattedInitialValues.alertRules);
|
||||
form.setFieldsValue({ ...formattedInitialValues });
|
||||
}, [form, formattedInitialValues, initialValues]);
|
||||
setSelectedTags(formatedInitialValues.alertRules);
|
||||
form.setFieldsValue({ ...formatedInitialValues });
|
||||
}, [form, formatedInitialValues, initialValues]);
|
||||
|
||||
const timeZoneItems: DefaultOptionType[] = ALL_TIME_ZONES.map(
|
||||
(timezone: string) => ({
|
||||
label: timezone,
|
||||
value: timezone,
|
||||
key: timezone,
|
||||
}),
|
||||
);
|
||||
|
||||
const getTimezoneFormattedTime = (
|
||||
time: string | dayjs.Dayjs,
|
||||
timeZone?: string,
|
||||
isEditMode?: boolean,
|
||||
format?: string,
|
||||
): string => {
|
||||
if (!time) {
|
||||
return '';
|
||||
}
|
||||
if (!timeZone) {
|
||||
return dayjs(time).format(format);
|
||||
}
|
||||
return dayjs(time).tz(timeZone, isEditMode).format(format);
|
||||
};
|
||||
|
||||
const startTimeText = useMemo((): string => {
|
||||
const startTime = formData.startTime;
|
||||
let startTime = formData?.startTime;
|
||||
if (recurrenceType !== recurrenceOptions.doesNotRepeat.value) {
|
||||
startTime =
|
||||
(formData?.recurrence?.startTime
|
||||
? dayjs(formData.recurrence.startTime).toISOString()
|
||||
: '') ||
|
||||
formData?.startTime ||
|
||||
'';
|
||||
}
|
||||
|
||||
if (!startTime) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const daysOfWeek = formData.recurrence?.repeatOn;
|
||||
if (formData.timezone) {
|
||||
startTime = handleTimeConversion(
|
||||
startTime,
|
||||
timezoneInitialValue,
|
||||
formData?.timezone,
|
||||
!isEditMode,
|
||||
);
|
||||
}
|
||||
const daysOfWeek = formData?.recurrence?.repeatOn;
|
||||
|
||||
const formattedStartTime = startTime.format(TIME_FORMAT);
|
||||
const formattedStartDate = startTime.format(DATE_FORMAT);
|
||||
const ordinalFormat = startTime.format(ORDINAL_FORMAT);
|
||||
const formattedStartTime = getTimezoneFormattedTime(
|
||||
startTime,
|
||||
formData.timezone,
|
||||
!isEditMode,
|
||||
TIME_FORMAT,
|
||||
);
|
||||
|
||||
const formattedStartDate = getTimezoneFormattedTime(
|
||||
startTime,
|
||||
formData.timezone,
|
||||
!isEditMode,
|
||||
DATE_FORMAT,
|
||||
);
|
||||
|
||||
const ordinalFormat = getTimezoneFormattedTime(
|
||||
startTime,
|
||||
formData.timezone,
|
||||
!isEditMode,
|
||||
ORDINAL_FORMAT,
|
||||
);
|
||||
|
||||
const formattedDaysOfWeek = daysOfWeek?.join(', ');
|
||||
switch (recurrenceType) {
|
||||
@@ -311,18 +388,49 @@ export function PlannedDowntimeForm(
|
||||
default:
|
||||
return `Scheduled for ${formattedStartDate} starting at ${formattedStartTime}.`;
|
||||
}
|
||||
}, [formData, recurrenceType, timezone]);
|
||||
}, [formData, recurrenceType, isEditMode, timezoneInitialValue]);
|
||||
|
||||
const endTimeText = useMemo((): string => {
|
||||
const endTime = formData.endTime;
|
||||
let endTime = formData?.endTime;
|
||||
if (recurrenceType !== recurrenceOptions.doesNotRepeat.value) {
|
||||
endTime =
|
||||
(formData?.recurrence?.endTime
|
||||
? dayjs(formData.recurrence.endTime).toISOString()
|
||||
: '') || '';
|
||||
|
||||
if (!isEditMode && !endTime) {
|
||||
endTime = formData?.endTime || '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!endTime) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const formattedEndTime = endTime.format(TIME_FORMAT);
|
||||
const formattedEndDate = endTime.format(DATE_FORMAT);
|
||||
if (formData.timezone) {
|
||||
endTime = handleTimeConversion(
|
||||
endTime,
|
||||
timezoneInitialValue,
|
||||
formData?.timezone,
|
||||
!isEditMode,
|
||||
);
|
||||
}
|
||||
|
||||
const formattedEndTime = getTimezoneFormattedTime(
|
||||
endTime,
|
||||
formData.timezone,
|
||||
!isEditMode,
|
||||
TIME_FORMAT,
|
||||
);
|
||||
|
||||
const formattedEndDate = getTimezoneFormattedTime(
|
||||
endTime,
|
||||
formData.timezone,
|
||||
!isEditMode,
|
||||
DATE_FORMAT,
|
||||
);
|
||||
return `Scheduled to end maintenance on ${formattedEndDate} at ${formattedEndTime}.`;
|
||||
}, [formData, recurrenceType, timezone]);
|
||||
}, [formData, recurrenceType, isEditMode, timezoneInitialValue]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -338,28 +446,33 @@ export function PlannedDowntimeForm(
|
||||
footer={null}
|
||||
>
|
||||
<Form<PlannedDowntimeFormData>
|
||||
name={isEditMode ? 'edit-form' : 'create-form'}
|
||||
name={initialValues.editMode ? 'edit-form' : 'create-form'}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
className="createForm"
|
||||
onFinish={onFinish}
|
||||
onValuesChange={(): void => {
|
||||
setRecurrenceType(form.getFieldValue('recurrence')?.repeatType as string);
|
||||
handleFormData(form.getFieldsValue());
|
||||
setFormData(form.getFieldsValue());
|
||||
}}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item label="Name" name="name" rules={requiredFieldRule}>
|
||||
<Form.Item label="Name" name="name" rules={formValidationRules}>
|
||||
<Input placeholder="e.g. Upgrade downtime" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Starts from"
|
||||
name="startTime"
|
||||
rules={requiredFieldRule}
|
||||
rules={formValidationRules}
|
||||
className={!isEmpty(startTimeText) ? 'formItemWithBullet' : ''}
|
||||
getValueProps={(value): any => ({
|
||||
value: value ? dayjs(value).tz(timezoneInitialValue) : undefined,
|
||||
})}
|
||||
>
|
||||
<DatePicker
|
||||
format={(date) => date.format(customFormat)}
|
||||
format={(date): string =>
|
||||
dayjs(date).tz(timezoneInitialValue).format(customFormat)
|
||||
}
|
||||
showTime
|
||||
renderExtraFooter={datePickerFooter}
|
||||
showNow={false}
|
||||
@@ -372,7 +485,7 @@ export function PlannedDowntimeForm(
|
||||
<Form.Item
|
||||
label="Repeats every"
|
||||
name={['recurrence', 'repeatType']}
|
||||
rules={requiredFieldRule}
|
||||
rules={formValidationRules}
|
||||
>
|
||||
<Select
|
||||
placeholder="Select option..."
|
||||
@@ -383,7 +496,7 @@ export function PlannedDowntimeForm(
|
||||
<Form.Item
|
||||
label="Weekly occurernce"
|
||||
name={['recurrence', 'repeatOn']}
|
||||
rules={requiredFieldRule}
|
||||
rules={formValidationRules}
|
||||
>
|
||||
<Select
|
||||
placeholder="Select option..."
|
||||
@@ -397,14 +510,16 @@ export function PlannedDowntimeForm(
|
||||
<Form.Item
|
||||
label="Duration"
|
||||
name={['recurrence', 'duration']}
|
||||
rules={requiredFieldRule}
|
||||
rules={formValidationRules}
|
||||
>
|
||||
<Input
|
||||
addonAfter={
|
||||
<Select
|
||||
defaultValue="m"
|
||||
value={durationUnit}
|
||||
onChange={(value): void => setDurationUnit(value)}
|
||||
onChange={(value): void => {
|
||||
setDurationUnit(value);
|
||||
}}
|
||||
>
|
||||
<Select.Option value="m">Mins</Select.Option>
|
||||
<Select.Option value="h">Hours</Select.Option>
|
||||
@@ -418,8 +533,8 @@ export function PlannedDowntimeForm(
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item label="Timezone" name="timezone" rules={requiredFieldRule}>
|
||||
<Select options={TZ_OPTIONS} placeholder="Select timezone" showSearch />
|
||||
<Form.Item label="Timezone" name="timezone" rules={formValidationRules}>
|
||||
<Select options={timeZoneItems} placeholder="Select timezone" showSearch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Ends on"
|
||||
@@ -431,9 +546,14 @@ export function PlannedDowntimeForm(
|
||||
},
|
||||
]}
|
||||
className={!isEmpty(endTimeText) ? 'formItemWithBullet' : ''}
|
||||
getValueProps={(value): any => ({
|
||||
value: value ? dayjs(value).tz(timezoneInitialValue) : undefined,
|
||||
})}
|
||||
>
|
||||
<DatePicker
|
||||
format={(date) => date.format(customFormat)}
|
||||
format={(date): string =>
|
||||
dayjs(date).tz(timezoneInitialValue).format(customFormat)
|
||||
}
|
||||
showTime
|
||||
showNow={false}
|
||||
renderExtraFooter={datePickerFooter}
|
||||
@@ -464,7 +584,7 @@ export function PlannedDowntimeForm(
|
||||
status={isError ? 'error' : undefined}
|
||||
loading={isLoading}
|
||||
tagRender={noTagRenderer}
|
||||
onChange={handleAlertRulesChange}
|
||||
onChange={handleChange}
|
||||
showSearch
|
||||
options={alertOptions}
|
||||
filterOption={(input, option): boolean =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Collapse, Flex, Space, Table, TableProps, Tag, Tooltip } from 'antd';
|
||||
@@ -7,8 +7,8 @@ import type { DefaultOptionType } from 'antd/es/select';
|
||||
import type {
|
||||
ListDowntimeSchedules200,
|
||||
RenderErrorResponseDTO,
|
||||
AlertmanagertypesPlannedMaintenanceDTO,
|
||||
AlertmanagertypesScheduleDTO,
|
||||
RuletypesPlannedMaintenanceDTO,
|
||||
RuletypesRecurrenceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { ErrorType } from 'api/generatedAPIInstance';
|
||||
import cx from 'classnames';
|
||||
@@ -19,11 +19,12 @@ import { CalendarClock, PenLine, Trash2 } from '@signozhq/icons';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import { showErrorNotification } from 'utils/error';
|
||||
import { showErrorNotification } from '../../utils/error';
|
||||
import {
|
||||
formatDateTime,
|
||||
getAlertOptionsFromIds,
|
||||
getDuration,
|
||||
getEndTime,
|
||||
recurrenceInfo,
|
||||
} from './PlannedDowntimeutils';
|
||||
|
||||
@@ -125,28 +126,29 @@ export function CollapseListContent({
|
||||
created_at,
|
||||
created_by_name,
|
||||
created_by_email,
|
||||
schedule,
|
||||
timeframe,
|
||||
repeats,
|
||||
updated_at,
|
||||
updated_by_name,
|
||||
alertOptions,
|
||||
timezone,
|
||||
}: {
|
||||
created_at?: string;
|
||||
created_by_name?: string;
|
||||
created_by_email?: string;
|
||||
schedule?: AlertmanagertypesScheduleDTO;
|
||||
timeframe: [string | undefined | null, string | undefined | null];
|
||||
repeats?: RuletypesRecurrenceDTO | null;
|
||||
updated_at?: string;
|
||||
updated_by_name?: string;
|
||||
alertOptions?: DefaultOptionType[];
|
||||
timezone?: string;
|
||||
}): JSX.Element {
|
||||
const repeats = schedule?.recurrence;
|
||||
const renderItems = (title: string, value: ReactNode): JSX.Element => (
|
||||
<div className="render-item-collapse-list">
|
||||
<Typography>{title}</Typography>
|
||||
<div className="render-item-value">{value}</div>
|
||||
</div>
|
||||
);
|
||||
const startTime = formatDateTime(schedule?.startTime, schedule?.timezone);
|
||||
const endTime = formatDateTime(schedule?.endTime, schedule?.timezone);
|
||||
|
||||
return (
|
||||
<Flex vertical>
|
||||
@@ -181,20 +183,16 @@ export function CollapseListContent({
|
||||
|
||||
{renderItems(
|
||||
'Timeframe',
|
||||
schedule?.startTime ? (
|
||||
<Typography>{`${startTime} ⎯ ${endTime}`}</Typography>
|
||||
timeframe[0] || timeframe[1] ? (
|
||||
<Typography>{`${formatDateTime(timeframe[0])} ⎯ ${formatDateTime(
|
||||
timeframe[1],
|
||||
)}`}</Typography>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
)}
|
||||
{renderItems(
|
||||
'Timezone',
|
||||
<Typography>{schedule?.timezone || '-'}</Typography>,
|
||||
)}
|
||||
{renderItems(
|
||||
'Repeats',
|
||||
<Typography>{recurrenceInfo(repeats, schedule?.timezone)}</Typography>,
|
||||
)}
|
||||
{renderItems('Timezone', <Typography>{timezone || '-'}</Typography>)}
|
||||
{renderItems('Repeats', <Typography>{recurrenceInfo(repeats)}</Typography>)}
|
||||
{renderItems(
|
||||
'Alerts silenced',
|
||||
alertOptions?.length ? (
|
||||
@@ -214,7 +212,7 @@ export function CollapseListContent({
|
||||
export function CustomCollapseList(
|
||||
props: DowntimeSchedulesTableData & {
|
||||
setInitialValues: React.Dispatch<
|
||||
React.SetStateAction<Partial<AlertmanagertypesPlannedMaintenanceDTO>>
|
||||
React.SetStateAction<Partial<RuletypesPlannedMaintenanceDTO>>
|
||||
>;
|
||||
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleDeleteDowntime: (id: string, name: string) => void;
|
||||
@@ -234,12 +232,22 @@ export function CustomCollapseList(
|
||||
setModalOpen,
|
||||
handleDeleteDowntime,
|
||||
setEditMode,
|
||||
kind,
|
||||
} = props;
|
||||
|
||||
const scheduleTime = schedule?.startTime
|
||||
? dayjs(schedule.startTime).tz(schedule.timezone)
|
||||
: createdAt || '';
|
||||
const formattedDateAndTime = `Start time ⎯ ${formatDateTime(scheduleTime)} ${schedule?.timezone}`;
|
||||
? dayjs(schedule.startTime).toISOString()
|
||||
: createdAt
|
||||
? dayjs(createdAt).toISOString()
|
||||
: '';
|
||||
// Combine time and date
|
||||
const formattedDateAndTime = `Start time ⎯ ${formatDateTime(
|
||||
defaultTo(scheduleTime, ''),
|
||||
)} ${schedule?.timezone}`;
|
||||
const endTime = getEndTime({
|
||||
kind,
|
||||
schedule,
|
||||
} as Partial<RuletypesPlannedMaintenanceDTO>);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -249,16 +257,21 @@ export function CustomCollapseList(
|
||||
<HeaderComponent
|
||||
duration={
|
||||
schedule?.recurrence?.duration
|
||||
? schedule.recurrence.duration
|
||||
: getDuration(schedule?.startTime || '', schedule?.endTime || '')
|
||||
? (schedule?.recurrence?.duration as string)
|
||||
: getDuration(
|
||||
schedule?.startTime ? dayjs(schedule.startTime).toISOString() : '',
|
||||
schedule?.endTime ? dayjs(schedule.endTime).toISOString() : '',
|
||||
)
|
||||
}
|
||||
name={defaultTo(name, '')}
|
||||
handleEdit={() => {
|
||||
handleEdit={(): void => {
|
||||
setInitialValues({ ...props });
|
||||
setModalOpen(true);
|
||||
setEditMode(true);
|
||||
}}
|
||||
handleDelete={() => handleDeleteDowntime(id ?? '', name || '')}
|
||||
handleDelete={(): void => {
|
||||
handleDeleteDowntime(id ?? '', name || '');
|
||||
}}
|
||||
/>
|
||||
}
|
||||
key={id ?? ''}
|
||||
@@ -266,10 +279,17 @@ export function CustomCollapseList(
|
||||
<CollapseListContent
|
||||
created_at={createdAt ? dayjs(createdAt).toISOString() : ''}
|
||||
created_by_name={defaultTo(createdBy, '')}
|
||||
schedule={schedule}
|
||||
timeframe={[
|
||||
schedule?.startTime?.toString(),
|
||||
typeof endTime === 'string' ? endTime : endTime?.toString(),
|
||||
]}
|
||||
repeats={
|
||||
schedule?.recurrence as RuletypesRecurrenceDTO | null | undefined
|
||||
}
|
||||
updated_at={updatedAt ? dayjs(updatedAt).toISOString() : ''}
|
||||
updated_by_name={defaultTo(updatedBy, '')}
|
||||
alertOptions={alertOptions}
|
||||
timezone={defaultTo(schedule?.timezone, '')}
|
||||
/>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
@@ -281,10 +301,9 @@ export function CustomCollapseList(
|
||||
);
|
||||
}
|
||||
|
||||
export type DowntimeSchedulesTableData =
|
||||
AlertmanagertypesPlannedMaintenanceDTO & {
|
||||
alertOptions: DefaultOptionType[];
|
||||
};
|
||||
export type DowntimeSchedulesTableData = RuletypesPlannedMaintenanceDTO & {
|
||||
alertOptions: DefaultOptionType[];
|
||||
};
|
||||
|
||||
export function PlannedDowntimeList({
|
||||
downtimeSchedules,
|
||||
@@ -301,7 +320,7 @@ export function PlannedDowntimeList({
|
||||
>;
|
||||
alertOptions: DefaultOptionType[];
|
||||
setInitialValues: React.Dispatch<
|
||||
React.SetStateAction<Partial<AlertmanagertypesPlannedMaintenanceDTO>>
|
||||
React.SetStateAction<Partial<RuletypesPlannedMaintenanceDTO>>
|
||||
>;
|
||||
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleDeleteDowntime: (id: string, name: string) => void;
|
||||
|
||||
@@ -5,14 +5,14 @@ import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import type {
|
||||
DeleteDowntimeScheduleByIDPathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
AlertmanagertypesPlannedMaintenanceDTO,
|
||||
AlertmanagertypesRecurrenceDTO,
|
||||
RuletypesPlannedMaintenanceDTO,
|
||||
RuletypesRecurrenceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { ErrorType } from 'api/generatedAPIInstance';
|
||||
import { AxiosError } from 'axios';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import dayjs from 'dayjs';
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
type DateTimeString = string | null | undefined;
|
||||
@@ -38,20 +38,14 @@ export const getDuration = (
|
||||
return `${hours} hours`;
|
||||
};
|
||||
|
||||
export const formatDateTime = (
|
||||
dateTimeString?: string | Dayjs | null,
|
||||
timezone?: string,
|
||||
): string => {
|
||||
export const formatDateTime = (dateTimeString?: string | null): string => {
|
||||
if (!dateTimeString) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
let dt = dayjs(dateTimeString);
|
||||
if (timezone) {
|
||||
dt = dt.tz(timezone);
|
||||
}
|
||||
|
||||
return dt.format(DATE_TIME_FORMATS.MONTH_DATETIME);
|
||||
return dayjs(dateTimeString.slice(0, 19)).format(
|
||||
DATE_TIME_FORMATS.MONTH_DATETIME,
|
||||
);
|
||||
};
|
||||
|
||||
export const getAlertOptionsFromIds = (
|
||||
@@ -66,8 +60,7 @@ export const getAlertOptionsFromIds = (
|
||||
);
|
||||
|
||||
export const recurrenceInfo = (
|
||||
recurrence?: AlertmanagertypesRecurrenceDTO | null,
|
||||
timezone?: string,
|
||||
recurrence?: RuletypesRecurrenceDTO | null,
|
||||
): string => {
|
||||
if (!recurrence) {
|
||||
return 'No';
|
||||
@@ -76,10 +69,10 @@ export const recurrenceInfo = (
|
||||
const { startTime, duration, repeatOn, repeatType, endTime } = recurrence;
|
||||
|
||||
const formattedStartTime = startTime
|
||||
? formatDateTime(startTime, timezone)
|
||||
? formatDateTime(dayjs(startTime).toISOString())
|
||||
: '';
|
||||
const formattedEndTime = endTime
|
||||
? `to ${formatDateTime(endTime, timezone)}`
|
||||
? `to ${formatDateTime(dayjs(endTime).toISOString())}`
|
||||
: '';
|
||||
const weeklyRepeatString = repeatOn ? `on ${repeatOn.join(', ')}` : '';
|
||||
const durationString = duration ? `- Duration: ${duration}` : '';
|
||||
@@ -87,20 +80,22 @@ export const recurrenceInfo = (
|
||||
return `Repeats - ${repeatType} ${weeklyRepeatString} from ${formattedStartTime} ${formattedEndTime} ${durationString}`;
|
||||
};
|
||||
|
||||
export const defaultInitialValues: Partial<AlertmanagertypesPlannedMaintenanceDTO> =
|
||||
{
|
||||
name: '',
|
||||
description: '',
|
||||
schedule: {
|
||||
timezone: '',
|
||||
endTime: undefined,
|
||||
recurrence: undefined,
|
||||
startTime: undefined,
|
||||
},
|
||||
alertIds: [],
|
||||
createdAt: undefined,
|
||||
createdBy: undefined,
|
||||
};
|
||||
export const defautlInitialValues: Partial<
|
||||
RuletypesPlannedMaintenanceDTO & { editMode: boolean }
|
||||
> = {
|
||||
name: '',
|
||||
description: '',
|
||||
schedule: {
|
||||
timezone: '',
|
||||
endTime: undefined,
|
||||
recurrence: undefined,
|
||||
startTime: undefined,
|
||||
},
|
||||
alertIds: [],
|
||||
createdAt: undefined,
|
||||
createdBy: undefined,
|
||||
editMode: false,
|
||||
};
|
||||
|
||||
type DeleteDowntimeScheduleProps = {
|
||||
deleteDowntimeScheduleAsync: UseMutateAsyncFunction<
|
||||
@@ -215,6 +210,75 @@ export const recurrenceOptionWithSubmenu: Option[] = [
|
||||
recurrenceOptions.monthly,
|
||||
];
|
||||
|
||||
export const getRecurrenceOptionFromValue = (
|
||||
value?: string | Option | null,
|
||||
): Option | null | undefined => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return Object.values(recurrenceOptions).find(
|
||||
(option) => option.value === value,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export const getEndTime = ({
|
||||
kind,
|
||||
schedule,
|
||||
}: Partial<
|
||||
RuletypesPlannedMaintenanceDTO & {
|
||||
editMode: boolean;
|
||||
}
|
||||
>): string | dayjs.Dayjs => {
|
||||
if (kind === 'fixed') {
|
||||
return schedule?.endTime ? dayjs(schedule.endTime).toISOString() : '';
|
||||
}
|
||||
|
||||
return schedule?.recurrence?.endTime
|
||||
? dayjs(schedule.recurrence.endTime).toISOString()
|
||||
: '';
|
||||
};
|
||||
|
||||
export const isScheduleRecurring = (
|
||||
schedule?: AlertmanagertypesPlannedMaintenanceDTO['schedule'] | null,
|
||||
schedule?: RuletypesPlannedMaintenanceDTO['schedule'] | null,
|
||||
): boolean => (schedule ? !isEmpty(schedule?.recurrence) : false);
|
||||
|
||||
function convertUtcOffsetToTimezoneOffset(offsetMinutes: number): string {
|
||||
const sign = offsetMinutes >= 0 ? '+' : '-';
|
||||
const absOffset = Math.abs(offsetMinutes);
|
||||
const hours = String(Math.floor(absOffset / 60)).padStart(2, '0');
|
||||
const minutes = String(absOffset % 60).padStart(2, '0');
|
||||
return `${sign}${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
export function formatWithTimezone(
|
||||
dateValue?: string | dayjs.Dayjs,
|
||||
timezone?: string,
|
||||
): string {
|
||||
const parsedDate =
|
||||
typeof dateValue === 'string' ? dateValue : dateValue?.format();
|
||||
|
||||
// Get the target timezone offset
|
||||
const targetOffset = convertUtcOffsetToTimezoneOffset(
|
||||
dayjs(dateValue).tz(timezone).utcOffset(),
|
||||
);
|
||||
|
||||
return `${parsedDate?.substring(0, 19)}${targetOffset}`;
|
||||
}
|
||||
|
||||
export function handleTimeConversion(
|
||||
dateValue: string | dayjs.Dayjs,
|
||||
timezoneInit?: string,
|
||||
timezone?: string,
|
||||
shouldKeepLocalTime?: boolean,
|
||||
): string {
|
||||
const timezoneChanged = !isEqual(timezoneInit, timezone);
|
||||
const initialTime = dayjs(dateValue).tz(timezoneInit);
|
||||
|
||||
const formattedTime = formatWithTimezone(initialTime, timezone);
|
||||
return timezoneChanged
|
||||
? formattedTime
|
||||
: dayjs(dateValue).tz(timezone, shouldKeepLocalTime).format();
|
||||
}
|
||||
|
||||
@@ -27,10 +27,10 @@ const MOCK_DATE_3 = '2024-01-03';
|
||||
const MOCK_DOWNTIME_1 = createMockDowntime({
|
||||
id: '1',
|
||||
name: MOCK_DOWNTIME_1_NAME,
|
||||
createdAt: MOCK_DATE_1,
|
||||
updatedAt: MOCK_DATE_1,
|
||||
createdAt: new Date(MOCK_DATE_1),
|
||||
updatedAt: new Date(MOCK_DATE_1),
|
||||
schedule: buildSchedule({
|
||||
startTime: MOCK_DATE_1,
|
||||
startTime: new Date(MOCK_DATE_1),
|
||||
timezone: 'UTC',
|
||||
}),
|
||||
alertIds: [],
|
||||
@@ -39,10 +39,10 @@ const MOCK_DOWNTIME_1 = createMockDowntime({
|
||||
const MOCK_DOWNTIME_2 = createMockDowntime({
|
||||
id: '2',
|
||||
name: MOCK_DOWNTIME_2_NAME,
|
||||
createdAt: MOCK_DATE_2,
|
||||
updatedAt: MOCK_DATE_2,
|
||||
createdAt: new Date(MOCK_DATE_2),
|
||||
updatedAt: new Date(MOCK_DATE_2),
|
||||
schedule: buildSchedule({
|
||||
startTime: MOCK_DATE_2,
|
||||
startTime: new Date(MOCK_DATE_2),
|
||||
timezone: 'UTC',
|
||||
}),
|
||||
alertIds: [],
|
||||
@@ -51,10 +51,10 @@ const MOCK_DOWNTIME_2 = createMockDowntime({
|
||||
const MOCK_DOWNTIME_3 = createMockDowntime({
|
||||
id: '3',
|
||||
name: MOCK_DOWNTIME_3_NAME,
|
||||
createdAt: MOCK_DATE_3,
|
||||
updatedAt: MOCK_DATE_3,
|
||||
createdAt: new Date(MOCK_DATE_3),
|
||||
updatedAt: new Date(MOCK_DATE_3),
|
||||
schedule: buildSchedule({
|
||||
startTime: MOCK_DATE_3,
|
||||
startTime: new Date(MOCK_DATE_3),
|
||||
timezone: 'UTC',
|
||||
}),
|
||||
alertIds: [],
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type {
|
||||
AlertmanagertypesScheduleDTO,
|
||||
AlertmanagertypesPlannedMaintenanceDTO,
|
||||
RuletypesPlannedMaintenanceDTO,
|
||||
RuletypesScheduleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
AlertmanagertypesMaintenanceKindDTO,
|
||||
AlertmanagertypesMaintenanceStatusDTO,
|
||||
RuletypesMaintenanceKindDTO,
|
||||
RuletypesMaintenanceStatusDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export const buildSchedule = (
|
||||
schedule: Partial<AlertmanagertypesScheduleDTO>,
|
||||
): AlertmanagertypesScheduleDTO => ({
|
||||
schedule: Partial<RuletypesScheduleDTO>,
|
||||
): RuletypesScheduleDTO => ({
|
||||
timezone: schedule?.timezone ?? '',
|
||||
startTime: schedule?.startTime,
|
||||
endTime: schedule?.endTime,
|
||||
@@ -17,14 +17,14 @@ export const buildSchedule = (
|
||||
});
|
||||
|
||||
export const createMockDowntime = (
|
||||
overrides: Partial<AlertmanagertypesPlannedMaintenanceDTO>,
|
||||
): AlertmanagertypesPlannedMaintenanceDTO => ({
|
||||
overrides: Partial<RuletypesPlannedMaintenanceDTO>,
|
||||
): RuletypesPlannedMaintenanceDTO => ({
|
||||
id: overrides.id ?? '0',
|
||||
name: overrides.name ?? '',
|
||||
description: overrides.description ?? '',
|
||||
schedule: buildSchedule({
|
||||
timezone: 'UTC',
|
||||
startTime: '2024-01-01',
|
||||
startTime: new Date('2024-01-01'),
|
||||
...overrides.schedule,
|
||||
}),
|
||||
alertIds: overrides.alertIds ?? [],
|
||||
@@ -32,6 +32,6 @@ export const createMockDowntime = (
|
||||
createdBy: overrides.createdBy ?? '',
|
||||
updatedAt: overrides.updatedAt,
|
||||
updatedBy: overrides.updatedBy ?? '',
|
||||
kind: overrides.kind ?? AlertmanagertypesMaintenanceKindDTO.recurring,
|
||||
status: overrides.status ?? AlertmanagertypesMaintenanceStatusDTO.active,
|
||||
kind: overrides.kind ?? RuletypesMaintenanceKindDTO.recurring,
|
||||
status: overrides.status ?? RuletypesMaintenanceStatusDTO.active,
|
||||
});
|
||||
|
||||
@@ -16,31 +16,35 @@ enum SpanScope {
|
||||
ENTRYPOINT_SPANS = 'entrypoint_spans',
|
||||
}
|
||||
|
||||
interface SpanFilterConfig {
|
||||
key: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface SpanScopeSelectorProps {
|
||||
onChange?: (value: TagFilter) => void;
|
||||
query?: IBuilderQuery;
|
||||
skipQueryBuilderRedirect?: boolean;
|
||||
}
|
||||
|
||||
const SPAN_FILTER_KEY: Record<SpanScope, string | null> = {
|
||||
const SPAN_FILTER_CONFIG: Record<SpanScope, SpanFilterConfig | null> = {
|
||||
[SpanScope.ALL_SPANS]: null,
|
||||
[SpanScope.ROOT_SPANS]: 'isRoot',
|
||||
[SpanScope.ENTRYPOINT_SPANS]: 'isEntryPoint',
|
||||
[SpanScope.ROOT_SPANS]: {
|
||||
key: 'isRoot',
|
||||
type: 'spanSearchScope',
|
||||
},
|
||||
[SpanScope.ENTRYPOINT_SPANS]: {
|
||||
key: 'isEntryPoint',
|
||||
type: 'spanSearchScope',
|
||||
},
|
||||
};
|
||||
|
||||
const SCOPE_FILTER_KEYS = Object.values(SPAN_FILTER_KEY).filter(
|
||||
(key): key is string => key !== null,
|
||||
);
|
||||
|
||||
const isScopeFilter = (filter: TagFilterItem, key: string): boolean =>
|
||||
filter.key?.key === key && String(filter.value) === 'true';
|
||||
|
||||
const createFilterItem = (key: string): TagFilterItem => ({
|
||||
const createFilterItem = (config: SpanFilterConfig): TagFilterItem => ({
|
||||
id: uuid().slice(0, 8),
|
||||
key: {
|
||||
key,
|
||||
key: config.key,
|
||||
dataType: undefined,
|
||||
type: '',
|
||||
type: config?.type,
|
||||
},
|
||||
op: '=',
|
||||
value: 'true',
|
||||
@@ -66,7 +70,12 @@ function SpanScopeSelector({
|
||||
filters: TagFilterItem[] = [],
|
||||
): SpanScope => {
|
||||
const hasFilter = (key: string): boolean =>
|
||||
filters?.some((filter) => isScopeFilter(filter, key));
|
||||
filters?.some(
|
||||
(filter) =>
|
||||
filter.key?.type === 'spanSearchScope' &&
|
||||
filter.key.key === key &&
|
||||
filter.value === 'true',
|
||||
);
|
||||
|
||||
if (hasFilter('isRoot')) {
|
||||
return SpanScope.ROOT_SPANS;
|
||||
@@ -104,21 +113,28 @@ function SpanScopeSelector({
|
||||
|
||||
const nonScopeFilters = currentFilters.filter(
|
||||
(filter) =>
|
||||
!SCOPE_FILTER_KEYS.some((scopeKey) => isScopeFilter(filter, scopeKey)),
|
||||
!(
|
||||
filter.key?.type === 'spanSearchScope' &&
|
||||
(filter.key.key === 'isRoot' || filter.key.key === 'isEntryPoint')
|
||||
),
|
||||
);
|
||||
|
||||
const scopeKey = SPAN_FILTER_KEY[newScope];
|
||||
const newScopeFilter = scopeKey !== null ? [createFilterItem(scopeKey)] : [];
|
||||
const config = SPAN_FILTER_CONFIG[newScope];
|
||||
const newScopeFilter = config !== null ? [createFilterItem(config)] : [];
|
||||
|
||||
return [...nonScopeFilters, ...newScopeFilter];
|
||||
};
|
||||
|
||||
const keysToRemove = Object.values(SPAN_FILTER_CONFIG)
|
||||
.map((config) => config?.key)
|
||||
.filter((key): key is string => typeof key === 'string');
|
||||
|
||||
newQuery.builder.queryData = newQuery.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
filter: {
|
||||
expression: removeKeysFromExpression(
|
||||
item.filter?.expression ?? '',
|
||||
SCOPE_FILTER_KEYS,
|
||||
keysToRemove,
|
||||
),
|
||||
},
|
||||
filters: {
|
||||
|
||||
@@ -20,16 +20,12 @@ import SpanScopeSelector from '../SpanScopeSelector';
|
||||
|
||||
const mockRedirectWithQueryBuilderData = jest.fn();
|
||||
|
||||
const SCOPE_KEYS = ['isRoot', 'isEntryPoint'];
|
||||
const isScopeFilter = (filter: TagFilterItem): boolean =>
|
||||
SCOPE_KEYS.includes(filter.key?.key ?? '') && String(filter.value) === 'true';
|
||||
|
||||
// Helper to create filter items
|
||||
const createSpanScopeFilter = (key: string): TagFilterItem => ({
|
||||
id: 'span-filter',
|
||||
key: {
|
||||
key,
|
||||
type: '',
|
||||
type: 'spanSearchScope',
|
||||
},
|
||||
op: '=',
|
||||
value: 'true',
|
||||
@@ -147,6 +143,7 @@ describe('SpanScopeSelector', () => {
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({
|
||||
key: expectedKey,
|
||||
type: 'spanSearchScope',
|
||||
}),
|
||||
op: '=',
|
||||
value: 'true',
|
||||
@@ -165,7 +162,11 @@ describe('SpanScopeSelector', () => {
|
||||
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalled();
|
||||
const updatedQuery = mockRedirectWithQueryBuilderData.mock.calls[0][0];
|
||||
const filters = updatedQuery.builder.queryData[0].filters.items;
|
||||
expect(filters.some(isScopeFilter)).toBe(false);
|
||||
expect(filters).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ type: 'spanSearchScope' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should add isRoot filter when selecting ROOT_SPANS', async () => {
|
||||
@@ -205,27 +206,6 @@ describe('SpanScopeSelector', () => {
|
||||
await expect(screen.findByText(expectedText)).resolves.toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
// Round-trip from filter.expression can deserialize the value as a boolean
|
||||
// `true` (unquoted in the expression) instead of the string `'true'` produced
|
||||
// by the dropdown. The dropdown must still recognize that as the scope filter.
|
||||
it.each([
|
||||
['Root Spans', 'isRoot'],
|
||||
['Entrypoint Spans', 'isEntryPoint'],
|
||||
])(
|
||||
'should initialize with %s selected when %s = true (boolean value)',
|
||||
async (expectedText, filterKey) => {
|
||||
const booleanScopeFilter: TagFilterItem = {
|
||||
id: 'span-filter',
|
||||
key: { key: filterKey, type: '' },
|
||||
op: '=',
|
||||
value: true as unknown as string,
|
||||
};
|
||||
const queryWithFilter = createQueryWithFilters([booleanScopeFilter]);
|
||||
renderWithContext(queryWithFilter, undefined, defaultQueryBuilderQuery);
|
||||
await expect(screen.findByText(expectedText)).resolves.toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('when onChange and query props are provided', () => {
|
||||
@@ -253,7 +233,9 @@ describe('SpanScopeSelector', () => {
|
||||
expect(items).toContainEqual(nonScopeItem);
|
||||
});
|
||||
|
||||
const scopeFiltersInPayload = items.filter(isScopeFilter);
|
||||
const scopeFiltersInPayload = items.filter(
|
||||
(filter) => filter.key?.type === 'spanSearchScope',
|
||||
);
|
||||
|
||||
if (expectedScopeKey) {
|
||||
expect(scopeFiltersInPayload).toHaveLength(1);
|
||||
@@ -452,7 +434,9 @@ describe('SpanScopeSelector', () => {
|
||||
items: [],
|
||||
};
|
||||
// Count non-scope filters
|
||||
const nonScopeFilters = items.filter((filter) => !isScopeFilter(filter));
|
||||
const nonScopeFilters = items.filter(
|
||||
(filter) => filter.key?.type !== 'spanSearchScope',
|
||||
);
|
||||
expect(nonScopeFilters).toHaveLength(1);
|
||||
|
||||
expect(nonScopeFilters).toContainEqual(
|
||||
|
||||
@@ -33,12 +33,14 @@ function TopNav(): JSX.Element | null {
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const isAlertCreationPage = useMemo(
|
||||
() => matchPath(location.pathname, { path: ROUTES.ALERTS_NEW, exact: true }),
|
||||
[location.pathname],
|
||||
const isNewAlertsLandingPage = useMemo(
|
||||
() =>
|
||||
matchPath(location.pathname, { path: ROUTES.ALERTS_NEW, exact: true }) &&
|
||||
!location.search,
|
||||
[location.pathname, location.search],
|
||||
);
|
||||
|
||||
if (isSignUpPage || isDisabled || isRouteToSkip || isAlertCreationPage) {
|
||||
if (isSignUpPage || isDisabled || isRouteToSkip || isNewAlertsLandingPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ const orgId = '019ba2bb-2fa1-7b24-8159-cfca08617ef9';
|
||||
export const managedRoles: AuthtypesRoleDTO[] = [
|
||||
{
|
||||
id: '019c24aa-2248-756f-9833-984f1ab63819',
|
||||
createdAt: '2026-02-03T18:00:55.624356Z',
|
||||
updatedAt: '2026-02-03T18:00:55.624356Z',
|
||||
createdAt: new Date('2026-02-03T18:00:55.624356Z'),
|
||||
updatedAt: new Date('2026-02-03T18:00:55.624356Z'),
|
||||
name: 'signoz-admin',
|
||||
description:
|
||||
'Role assigned to users who have full administrative access to SigNoz resources.',
|
||||
@@ -15,8 +15,8 @@ export const managedRoles: AuthtypesRoleDTO[] = [
|
||||
},
|
||||
{
|
||||
id: '019c24aa-2248-757c-9faf-7b1e899751e0',
|
||||
createdAt: '2026-02-03T18:00:55.624359Z',
|
||||
updatedAt: '2026-02-03T18:00:55.624359Z',
|
||||
createdAt: new Date('2026-02-03T18:00:55.624359Z'),
|
||||
updatedAt: new Date('2026-02-03T18:00:55.624359Z'),
|
||||
name: 'signoz-editor',
|
||||
description:
|
||||
'Role assigned to users who can create, edit, and manage SigNoz resources but do not have full administrative privileges.',
|
||||
@@ -25,8 +25,8 @@ export const managedRoles: AuthtypesRoleDTO[] = [
|
||||
},
|
||||
{
|
||||
id: '019c24aa-2248-7585-a129-4188b3473c27',
|
||||
createdAt: '2026-02-03T18:00:55.624362Z',
|
||||
updatedAt: '2026-02-03T18:00:55.624362Z',
|
||||
createdAt: new Date('2026-02-03T18:00:55.624362Z'),
|
||||
updatedAt: new Date('2026-02-03T18:00:55.624362Z'),
|
||||
name: 'signoz-viewer',
|
||||
description:
|
||||
'Role assigned to users who have read-only access to SigNoz resources.',
|
||||
@@ -38,8 +38,8 @@ export const managedRoles: AuthtypesRoleDTO[] = [
|
||||
export const customRoles: AuthtypesRoleDTO[] = [
|
||||
{
|
||||
id: '019c24aa-3333-0001-aaaa-111111111111',
|
||||
createdAt: '2026-02-10T10:30:00.000Z',
|
||||
updatedAt: '2026-02-12T14:20:00.000Z',
|
||||
createdAt: new Date('2026-02-10T10:30:00.000Z'),
|
||||
updatedAt: new Date('2026-02-12T14:20:00.000Z'),
|
||||
name: 'billing-manager',
|
||||
description: 'Custom role for managing billing and invoices.',
|
||||
type: 'custom',
|
||||
@@ -47,8 +47,8 @@ export const customRoles: AuthtypesRoleDTO[] = [
|
||||
},
|
||||
{
|
||||
id: '019c24aa-3333-0002-bbbb-222222222222',
|
||||
createdAt: '2026-02-11T09:00:00.000Z',
|
||||
updatedAt: '2026-02-13T11:45:00.000Z',
|
||||
createdAt: new Date('2026-02-11T09:00:00.000Z'),
|
||||
updatedAt: new Date('2026-02-13T11:45:00.000Z'),
|
||||
name: 'dashboard-creator',
|
||||
description: 'Custom role allowing users to create and manage dashboards.',
|
||||
type: 'custom',
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
import ConversationView from 'container/AIAssistant/ConversationView';
|
||||
import { AIAssistantEvents } from 'container/AIAssistant/events';
|
||||
import { normalizePage } from 'container/AIAssistant/hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
|
||||
import { VariantContext } from 'container/AIAssistant/VariantContext';
|
||||
import { Sparkles } from '@signozhq/icons';
|
||||
@@ -20,27 +17,8 @@ interface RouteParams {
|
||||
|
||||
export default function AIAssistantPage(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const location = useLocation<{ fromInApp?: boolean } | undefined>();
|
||||
const { pathname } = location;
|
||||
const { conversationId } = useParams<RouteParams>();
|
||||
|
||||
// Skip the mount-time Opened fire when the user expanded an already-open
|
||||
// drawer/modal — that surface already emitted Opened with the right source.
|
||||
// Router state (vs a module flag) survives StrictMode double-mount and
|
||||
// aborted navigations.
|
||||
const fromInApp = location.state?.fromInApp === true;
|
||||
useEffect(() => {
|
||||
if (fromInApp) {
|
||||
return;
|
||||
}
|
||||
void logEvent(AIAssistantEvents.Opened, {
|
||||
source: 'deeplink',
|
||||
currentPage: normalizePage(pathname),
|
||||
});
|
||||
// Only on mount; route param changes inside the same page aren't a re-open.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const conversations = useAIAssistantStore((s) => s.conversations);
|
||||
const activeConversationId = useAIAssistantStore(
|
||||
(s) => s.activeConversationId,
|
||||
@@ -93,14 +71,9 @@ export default function AIAssistantPage(): JSX.Element {
|
||||
);
|
||||
|
||||
const handleNewConversation = useCallback(() => {
|
||||
void logEvent(AIAssistantEvents.NewChatClicked, {
|
||||
page: normalizePage(pathname),
|
||||
mode: 'full_screen',
|
||||
source: 'history_list',
|
||||
});
|
||||
const newId = startNewConversation();
|
||||
history.push(ROUTES.AI_ASSISTANT.replace(':conversationId', newId));
|
||||
}, [startNewConversation, history, pathname]);
|
||||
}, [startNewConversation, history]);
|
||||
|
||||
// Prefer the URL param, but fall back to the store's `activeConversationId`
|
||||
// for the brief render after a re-key (client UUID → backend threadId), so
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
}
|
||||
|
||||
.alert-details {
|
||||
margin-top: 10px;
|
||||
.divider {
|
||||
border-color: var(--l1-border);
|
||||
margin: 16px 0;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Divider } from 'antd';
|
||||
import { Breadcrumb, Button, Divider } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import classNames from 'classnames';
|
||||
import AlertBreadcrumb from 'components/AlertBreadcrumb';
|
||||
import { Filters } from 'components/AlertDetailsFilters/Filters';
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import Spinner from 'components/Spinner';
|
||||
@@ -11,12 +10,13 @@ import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { CreateAlertProvider } from 'container/CreateAlertV2/context';
|
||||
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { useAlertRule } from 'providers/Alert';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { NEW_ALERT_SCHEMA_VERSION } from 'types/api/alerts/alertTypesV2';
|
||||
import { fromRuleDTOToPostableRuleV2 } from 'types/api/alerts/convert';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import AlertHeader from './AlertHeader/AlertHeader';
|
||||
import AlertNotFound from './AlertNotFound';
|
||||
@@ -24,11 +24,42 @@ import { useGetAlertRuleDetails, useRouteTabUtils } from './hooks';
|
||||
|
||||
import './AlertDetails.styles.scss';
|
||||
|
||||
function BreadCrumbItem({
|
||||
title,
|
||||
isLast,
|
||||
route,
|
||||
}: {
|
||||
title: string | null;
|
||||
isLast?: boolean;
|
||||
route?: string;
|
||||
}): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
if (isLast) {
|
||||
return <div className="breadcrumb-item breadcrumb-item--last">{title}</div>;
|
||||
}
|
||||
const handleNavigate = (e: React.MouseEvent): void => {
|
||||
if (!route) {
|
||||
return;
|
||||
}
|
||||
safeNavigate(ROUTES.LIST_ALL_ALERT, { newTab: isModifierKeyPressed(e) });
|
||||
};
|
||||
|
||||
return (
|
||||
<Button type="text" className="breadcrumb-item" onClick={handleNavigate}>
|
||||
{title}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
BreadCrumbItem.defaultProps = {
|
||||
isLast: false,
|
||||
route: '',
|
||||
};
|
||||
|
||||
function AlertDetails(): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const { routes } = useRouteTabUtils();
|
||||
const params = useUrlQuery();
|
||||
const { alertRuleName } = useAlertRule();
|
||||
|
||||
const { isLoading, isError, ruleId, isValidRuleId, alertDetailsResponse } =
|
||||
useGetAlertRuleDetails();
|
||||
@@ -38,7 +69,7 @@ function AlertDetails(): JSX.Element {
|
||||
}, [params]);
|
||||
|
||||
const getDocumentTitle = useMemo(() => {
|
||||
const alertTitle = alertRuleName ?? alertDetailsResponse?.data?.alert;
|
||||
const alertTitle = alertDetailsResponse?.data?.alert;
|
||||
if (alertTitle) {
|
||||
return alertTitle;
|
||||
}
|
||||
@@ -49,7 +80,7 @@ function AlertDetails(): JSX.Element {
|
||||
return document.title;
|
||||
}
|
||||
return 'Alert Not Found';
|
||||
}, [alertRuleName, alertDetailsResponse?.data?.alert, isTestAlert, isLoading]);
|
||||
}, [alertDetailsResponse?.data?.alert, isTestAlert, isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = getDocumentTitle;
|
||||
@@ -95,13 +126,20 @@ function AlertDetails(): JSX.Element {
|
||||
<div
|
||||
className={classNames('alert-details', { 'alert-details-v2': isV2Alert })}
|
||||
>
|
||||
<AlertBreadcrumb
|
||||
<Breadcrumb
|
||||
className="alert-details__breadcrumb"
|
||||
items={[
|
||||
{ title: 'Alert Rules', route: ROUTES.LIST_ALL_ALERT },
|
||||
{ title: ruleId, isLast: true },
|
||||
{
|
||||
title: (
|
||||
<BreadCrumbItem title="Alert Rules" route={ROUTES.LIST_ALL_ALERT} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <BreadCrumbItem title={ruleId} isLast />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Divider className="divider breadcrumb-divider" />
|
||||
|
||||
{alertRuleDetails && <AlertHeader alertDetails={alertRuleDetails} />}
|
||||
<Divider className="divider" />
|
||||
|
||||
@@ -33,12 +33,13 @@ const menuItemStyleV2: CSSProperties = {
|
||||
function AlertActionButtons({
|
||||
ruleId,
|
||||
alertDetails,
|
||||
setUpdatedName,
|
||||
}: {
|
||||
ruleId: string;
|
||||
alertDetails: AlertHeaderProps['alertDetails'];
|
||||
setUpdatedName: (name: string) => void;
|
||||
}): JSX.Element {
|
||||
const { alertRuleState, setAlertRuleState, alertRuleName, setAlertRuleName } =
|
||||
useAlertRule();
|
||||
const { alertRuleState, setAlertRuleState } = useAlertRule();
|
||||
const [intermediateName, setIntermediateName] = useState<string>(
|
||||
alertDetails.alert,
|
||||
);
|
||||
@@ -52,7 +53,7 @@ function AlertActionButtons({
|
||||
const { handleAlertDelete } = useAlertRuleDelete({ ruleId });
|
||||
const { handleAlertUpdate, isLoading } = useAlertRuleUpdate({
|
||||
alertDetails: alertDetails as unknown as AlertDef,
|
||||
setAlertRuleName,
|
||||
setUpdatedName,
|
||||
intermediateName,
|
||||
});
|
||||
|
||||
@@ -112,12 +113,6 @@ function AlertActionButtons({
|
||||
}
|
||||
}, [setAlertRuleState, alertRuleState, alertDetails.state]);
|
||||
|
||||
useEffect(() => {
|
||||
if (alertRuleName !== undefined) {
|
||||
setIntermediateName(alertRuleName);
|
||||
}
|
||||
}, [alertRuleName]);
|
||||
|
||||
// on unmount remove the alert state
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => (): void => setAlertRuleState(undefined), []);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user