mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-26 03:40:33 +01:00
Compare commits
24 Commits
fix/extern
...
refactor/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f02a515c68 | ||
|
|
ffa33f71e1 | ||
|
|
d1f143f675 | ||
|
|
51820ee87c | ||
|
|
1355b13504 | ||
|
|
36f29ccced | ||
|
|
22d6d5248f | ||
|
|
fdbdbf27a8 | ||
|
|
c1e81a57a3 | ||
|
|
c30994f966 | ||
|
|
9aef69a186 | ||
|
|
66bc4b4398 | ||
|
|
04c58a1572 | ||
|
|
9bbdd00858 | ||
|
|
316e9c7361 | ||
|
|
634166860b | ||
|
|
af8f2fa95a | ||
|
|
f81fd78ff6 | ||
|
|
b589a7b2e9 | ||
|
|
716dbc7847 | ||
|
|
3a92c7577f | ||
|
|
ba043a5741 | ||
|
|
6d2b99eb8d | ||
|
|
3765ca3d42 |
17
.github/workflows/goci.yaml
vendored
17
.github/workflows/goci.yaml
vendored
@@ -123,3 +123,20 @@ jobs:
|
||||
run: |
|
||||
go run cmd/enterprise/*.go generate authz
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in authz permissions. Run go run cmd/enterprise/*.go generate authz locally and commit."; exit 1)
|
||||
web-settings:
|
||||
if: |
|
||||
github.event_name == 'merge_group' ||
|
||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: self-checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: go-install
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
- name: generate-web-settings
|
||||
run: |
|
||||
go run cmd/enterprise/*.go generate config web-settings
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in web settings schema. Run go run cmd/enterprise/*.go generate config web-settings locally and commit."; exit 1)
|
||||
|
||||
23
.github/workflows/jsci.yaml
vendored
23
.github/workflows/jsci.yaml
vendored
@@ -90,3 +90,26 @@ jobs:
|
||||
run: |
|
||||
cd frontend && pnpm generate:api
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated api clients. Run pnpm generate:api in frontend/ locally and commit."; exit 1)
|
||||
web-settings:
|
||||
if: |
|
||||
github.event_name == 'merge_group' ||
|
||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: self-checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: node-install
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: install-pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10
|
||||
- name: install-frontend
|
||||
run: cd frontend && pnpm install
|
||||
- name: generate-web-settings
|
||||
run: |
|
||||
cd frontend && pnpm generate:config:web-settings
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated web settings types. Run pnpm generate:config:web-settings in frontend/ locally and commit."; exit 1)
|
||||
|
||||
61
cmd/genconfig.go
Normal file
61
cmd/genconfig.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/web"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
const webSettingsSchemaPath = "docs/config/web-settings.json"
|
||||
|
||||
func registerGenerateConfig(parentCmd *cobra.Command) {
|
||||
configCmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Generate JSON Schema for config",
|
||||
}
|
||||
|
||||
configCmd.AddCommand(&cobra.Command{
|
||||
Use: "web-settings",
|
||||
Short: "Generate JSON Schema for web settings",
|
||||
RunE: func(currCmd *cobra.Command, args []string) error {
|
||||
return generateWebSettings()
|
||||
},
|
||||
})
|
||||
|
||||
parentCmd.AddCommand(configCmd)
|
||||
}
|
||||
|
||||
func generateWebSettings() error {
|
||||
falseVal := false
|
||||
noAdditional := jsonschema.SchemaOrBool{TypeBoolean: &falseVal}
|
||||
|
||||
reflector := jsonschema.Reflector{}
|
||||
reflector.DefaultOptions = append(reflector.DefaultOptions,
|
||||
jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (bool, error) {
|
||||
if params.Value.Kind() == reflect.Struct {
|
||||
params.Schema.AdditionalProperties = &noAdditional
|
||||
}
|
||||
return false, nil
|
||||
}),
|
||||
jsonschema.InterceptDefName(func(t reflect.Type, defaultDefName string) string {
|
||||
return strings.TrimPrefix(defaultDefName, "Web")
|
||||
}),
|
||||
)
|
||||
|
||||
schema, err := reflector.Reflect(web.Settings{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(schema, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(webSettingsSchemaPath, append(data, '\n'), 0o600)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ func RegisterGenerate(parentCmd *cobra.Command, logger *slog.Logger) {
|
||||
|
||||
registerGenerateOpenAPI(generateCmd)
|
||||
registerGenerateAuthz(generateCmd)
|
||||
registerGenerateConfig(generateCmd)
|
||||
|
||||
parentCmd.AddCommand(generateCmd)
|
||||
}
|
||||
|
||||
42
docs/config/web-settings.json
Normal file
42
docs/config/web-settings.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"required": [
|
||||
"posthog",
|
||||
"appcues"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Appcues": {
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Posthog": {
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"appcues": {
|
||||
"$ref": "#/definitions/Appcues"
|
||||
},
|
||||
"posthog": {
|
||||
"$ref": "#/definitions/Posthog"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
@@ -47,10 +47,10 @@ const config: Config.InitialOptions = {
|
||||
transformIgnorePatterns: [
|
||||
// @chenglou/pretext is ESM-only; @signozhq/ui pulls it in via text-ellipsis.
|
||||
// Pattern 1: allow .pnpm virtual store through (handled by pattern 2), plus root-level ESM packages.
|
||||
'node_modules/(?!(\\.pnpm|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou/pretext|@signozhq/design-tokens|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid)/)',
|
||||
'node_modules/(?!(\\.pnpm|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou/pretext|@signozhq/design-tokens|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard)/)',
|
||||
// Pattern 2: pnpm virtual store — ignore everything except ESM-only packages.
|
||||
// pnpm encodes scoped packages as @scope+name@version, so match on scope prefix.
|
||||
'node_modules/\\.pnpm/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid)[^/]*/node_modules)',
|
||||
'node_modules/\\.pnpm/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard)[^/]*/node_modules)',
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"commitlint": "commitlint --edit $1",
|
||||
"test": "jest",
|
||||
"test:changedsince": "jest --changedSince=main --coverage --silent",
|
||||
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh"
|
||||
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
|
||||
"generate:config:web-settings": "json2ts ../docs/config/web-settings.json -o src/types/generated/webSettings.ts --style.useTabs --style.tabWidth=1 --style.singleQuote --bannerComment '/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM docs/config/web-settings.json */'"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
@@ -49,7 +50,7 @@
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/icons": "0.4.0",
|
||||
"@signozhq/resizable": "0.0.2",
|
||||
"@signozhq/ui": "0.0.21",
|
||||
"@signozhq/ui": "0.0.22",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
@@ -160,8 +161,8 @@
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/event-source-polyfill": "^1.0.0",
|
||||
"@types/d3-hierarchy": "1.1.11",
|
||||
"@types/event-source-polyfill": "^1.0.0",
|
||||
"@types/history": "4.7.11",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
@@ -187,6 +188,7 @@
|
||||
"is-ci": "^3.0.1",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jest-styled-components": "^7.2.0",
|
||||
"json-schema-to-typescript": "^15.0.4",
|
||||
"lint-staged": "^17.0.4",
|
||||
"msw": "1.3.2",
|
||||
"orval": "8.9.1",
|
||||
@@ -241,4 +243,4 @@
|
||||
"tmp": "0.2.4",
|
||||
"vite": "npm:rolldown-vite@7.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ const BANNED_COMPONENTS = {
|
||||
Typography:
|
||||
'Use @signozhq/ui/typography Typography instead of antd Typography.',
|
||||
Switch: 'Use @signozhq/ui/switch Switch instead of antd Switch.',
|
||||
Dropdown:
|
||||
'Use @signozhq/ui DropdownMenuSimple (or the composable DropdownMenu primitives) from @signozhq/ui/dropdown-menu instead of antd Dropdown.',
|
||||
Badge: 'Use @signozhq/ui/badge instead of antd Badge.',
|
||||
Progress: 'Use @signozhq/ui/progress instead of antd Progress.',
|
||||
};
|
||||
|
||||
58
frontend/pnpm-lock.yaml
generated
58
frontend/pnpm-lock.yaml
generated
@@ -77,8 +77,8 @@ importers:
|
||||
specifier: 0.0.2
|
||||
version: 0.0.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@signozhq/ui':
|
||||
specifier: 0.0.21
|
||||
version: 0.0.21(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
|
||||
specifier: 0.0.22
|
||||
version: 0.0.22(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
|
||||
'@tanstack/react-table':
|
||||
specifier: 8.21.3
|
||||
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -449,6 +449,9 @@ importers:
|
||||
jest-styled-components:
|
||||
specifier: ^7.2.0
|
||||
version: 7.2.0(styled-components@5.3.11(react-dom@18.2.0(react@18.2.0))(react-is@19.2.6)(react@18.2.0))
|
||||
json-schema-to-typescript:
|
||||
specifier: ^15.0.4
|
||||
version: 15.0.4
|
||||
lint-staged:
|
||||
specifier: ^17.0.4
|
||||
version: 17.0.4
|
||||
@@ -457,7 +460,7 @@ importers:
|
||||
version: 1.3.2(typescript@5.9.3)
|
||||
orval:
|
||||
specifier: 8.9.1
|
||||
version: 8.9.1(typescript@5.9.3)
|
||||
version: 8.9.1(prettier@3.8.3)(typescript@5.9.3)
|
||||
oxfmt:
|
||||
specifier: 0.47.0
|
||||
version: 0.47.0
|
||||
@@ -545,6 +548,10 @@ packages:
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
|
||||
'@apidevtools/json-schema-ref-parser@11.9.3':
|
||||
resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -1991,6 +1998,9 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
|
||||
'@jsdevtools/ono@7.1.3':
|
||||
resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
|
||||
|
||||
'@keyv/bigmap@1.3.1':
|
||||
resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==}
|
||||
engines: {node: '>= 18'}
|
||||
@@ -3269,8 +3279,8 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18.2.0
|
||||
|
||||
'@signozhq/ui@0.0.21':
|
||||
resolution: {integrity: sha512-uLM3Vqwxlk2USXbwtb3qRLpjZR9b9QSHFQq/jtcfYNMDmIE/sNjSj0nRkEhX4RqqRgsLRt2PVA33aeWxDOLO3g==}
|
||||
'@signozhq/ui@0.0.22':
|
||||
resolution: {integrity: sha512-CJDyA4H+uXG/U2/d7/nRMNY6WIW0YWc843mfzUQALjm+xOhbO4T+qt67THjV4s1wTMs1cZLkmScbMddf+hXLIQ==}
|
||||
peerDependencies:
|
||||
'@signozhq/icons': 0.3.0
|
||||
react: ^18.2.0
|
||||
@@ -6066,6 +6076,11 @@ packages:
|
||||
json-parse-even-better-errors@2.3.1:
|
||||
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||
|
||||
json-schema-to-typescript@15.0.4:
|
||||
resolution: {integrity: sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
hasBin: true
|
||||
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
@@ -7104,6 +7119,11 @@ packages:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
prettier@3.8.3:
|
||||
resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
pretty-format@27.5.1:
|
||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||
@@ -9044,6 +9064,12 @@ snapshots:
|
||||
resize-observer-polyfill: 1.5.1
|
||||
throttle-debounce: 5.0.0
|
||||
|
||||
'@apidevtools/json-schema-ref-parser@11.9.3':
|
||||
dependencies:
|
||||
'@jsdevtools/ono': 7.1.3
|
||||
'@types/json-schema': 7.0.15
|
||||
js-yaml: 4.1.1
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
@@ -10798,6 +10824,8 @@ snapshots:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
optional: true
|
||||
|
||||
'@jsdevtools/ono@7.1.3': {}
|
||||
|
||||
'@keyv/bigmap@1.3.1(keyv@5.6.0)':
|
||||
dependencies:
|
||||
hashery: 1.5.1
|
||||
@@ -12013,7 +12041,7 @@ snapshots:
|
||||
- react-dom
|
||||
- tailwindcss
|
||||
|
||||
'@signozhq/ui@0.0.21(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
|
||||
'@signozhq/ui@0.0.22(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@chenglou/pretext': 0.0.5
|
||||
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -15374,6 +15402,18 @@ snapshots:
|
||||
|
||||
json-parse-even-better-errors@2.3.1: {}
|
||||
|
||||
json-schema-to-typescript@15.0.4:
|
||||
dependencies:
|
||||
'@apidevtools/json-schema-ref-parser': 11.9.3
|
||||
'@types/json-schema': 7.0.15
|
||||
'@types/lodash': 4.17.24
|
||||
is-glob: 4.0.3
|
||||
js-yaml: 4.1.1
|
||||
lodash: 4.18.1
|
||||
minimist: 1.2.8
|
||||
prettier: 3.8.3
|
||||
tinyglobby: 0.2.15
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
@@ -16290,7 +16330,7 @@ snapshots:
|
||||
strip-ansi: 6.0.1
|
||||
wcwidth: 1.0.1
|
||||
|
||||
orval@8.9.1(typescript@5.9.3):
|
||||
orval@8.9.1(prettier@3.8.3)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@commander-js/extra-typings': 14.0.0(commander@14.0.2)
|
||||
'@orval/angular': 8.9.1(typescript@5.9.3)
|
||||
@@ -16321,6 +16361,8 @@ snapshots:
|
||||
typedoc: 0.28.19(typescript@5.9.3)
|
||||
typedoc-plugin-coverage: 4.0.2(typedoc@0.28.19(typescript@5.9.3))
|
||||
typedoc-plugin-markdown: 4.11.0(typedoc@0.28.19(typescript@5.9.3))
|
||||
optionalDependencies:
|
||||
prettier: 3.8.3
|
||||
transitivePeerDependencies:
|
||||
- '@faker-js/faker'
|
||||
- supports-color
|
||||
@@ -16581,6 +16623,8 @@ snapshots:
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@3.8.3: {}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
.dropdown-button {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Ellipsis } from '@signozhq/icons';
|
||||
import { Button, Dropdown, MenuProps } from 'antd';
|
||||
|
||||
import './DropDown.styles.scss';
|
||||
|
||||
function DropDown({
|
||||
element,
|
||||
onDropDownItemClick,
|
||||
}: {
|
||||
element: JSX.Element[];
|
||||
onDropDownItemClick?: MenuProps['onClick'];
|
||||
}): JSX.Element {
|
||||
const items: MenuProps['items'] = element.map(
|
||||
(e: JSX.Element, index: number) => ({
|
||||
label: e,
|
||||
key: index,
|
||||
}),
|
||||
);
|
||||
|
||||
const [isDdOpen, setDdOpen] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items,
|
||||
onMouseEnter: (): void => setDdOpen(true),
|
||||
onMouseLeave: (): void => setDdOpen(false),
|
||||
onClick: (item): void => onDropDownItemClick?.(item),
|
||||
}}
|
||||
open={isDdOpen}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
className={`dropdown-button`}
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
setDdOpen(true);
|
||||
}}
|
||||
>
|
||||
<Ellipsis className="dropdown-icon" size={16} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
DropDown.defaultProps = {
|
||||
onDropDownItemClick: (): void => {},
|
||||
};
|
||||
|
||||
export default DropDown;
|
||||
@@ -1,15 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Dropdown,
|
||||
MenuProps,
|
||||
Popover,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
} from 'antd';
|
||||
import { Button, Col, Popover, Row, Select, Space } from 'antd';
|
||||
import { DropdownMenuSimple, type MenuProps } from '@signozhq/ui/dropdown-menu';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import axios from 'axios';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
@@ -241,9 +233,9 @@ function ExplorerCard({
|
||||
</Popover>
|
||||
<Share2 onClick={onCopyUrlHandler} size="md" />
|
||||
{viewKey && (
|
||||
<Dropdown trigger={['click']} menu={moreOptionMenu}>
|
||||
<Ellipsis size="md" />
|
||||
</Dropdown>
|
||||
<DropdownMenuSimple menu={moreOptionMenu}>
|
||||
<Button type="text" size="small" icon={<Ellipsis size="md" />} />
|
||||
</DropdownMenuSimple>
|
||||
)}
|
||||
</Space>
|
||||
</OffSetCol>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Dropdown } from 'antd';
|
||||
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||
import cx from 'classnames';
|
||||
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -195,7 +195,7 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
)}
|
||||
|
||||
{isMultiQueryAllowed && (
|
||||
<Dropdown
|
||||
<DropdownMenuSimple
|
||||
className="query-actions-dropdown"
|
||||
menu={{
|
||||
items: [
|
||||
@@ -217,10 +217,10 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
: []),
|
||||
],
|
||||
}}
|
||||
placement="bottomRight"
|
||||
align="end"
|
||||
>
|
||||
<Ellipsis size={16} />
|
||||
</Dropdown>
|
||||
</DropdownMenuSimple>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,6 +51,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.duration-input-slider {
|
||||
padding: 12px 0px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
|
||||
@@ -4,14 +4,14 @@ import type {
|
||||
TableColumnsType as ColumnsType,
|
||||
TableColumnType as ColumnType,
|
||||
} from 'antd';
|
||||
import { Button, Dropdown, Flex, MenuProps } from 'antd';
|
||||
import { Button, Flex } from 'antd';
|
||||
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { SlidersHorizontal } from '@signozhq/icons';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import ResizeTable from './ResizeTable';
|
||||
import { DynamicColumnTableProps } from './types';
|
||||
@@ -84,8 +84,9 @@ function DynamicColumnTable({
|
||||
);
|
||||
};
|
||||
|
||||
const items: MenuProps['items'] =
|
||||
const items: MenuItem[] =
|
||||
dynamicColumns?.map((column, index) => ({
|
||||
key: String(index),
|
||||
label: (
|
||||
<div
|
||||
className="dynamicColumnsTable-items"
|
||||
@@ -99,8 +100,6 @@ function DynamicColumnTable({
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
key: index,
|
||||
type: 'checkbox',
|
||||
})) || [];
|
||||
|
||||
// Get current page from URL or default to 1
|
||||
@@ -129,18 +128,14 @@ function DynamicColumnTable({
|
||||
<Flex justify="flex-end" align="center" gap={8}>
|
||||
{facingIssueBtn && <LaunchChatSupport {...facingIssueBtn} />}
|
||||
{dynamicColumns && (
|
||||
<Dropdown
|
||||
getPopupContainer={popupContainer}
|
||||
menu={{ items }}
|
||||
trigger={['click']}
|
||||
>
|
||||
<DropdownMenuSimple menu={{ items }}>
|
||||
<Button
|
||||
className="dynamicColumnTable-button filter-btn"
|
||||
size="middle"
|
||||
icon={<SlidersHorizontal size={14} />}
|
||||
data-testid="additional-filters-button"
|
||||
/>
|
||||
</Dropdown>
|
||||
</DropdownMenuSimple>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Dispatch, SetStateAction, useCallback, useMemo } from 'react';
|
||||
import { ChevronDown, Globe } from '@signozhq/icons';
|
||||
import { Button, Dropdown } from 'antd';
|
||||
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||
import { Button } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import TimeItems, {
|
||||
timePreferance,
|
||||
@@ -27,20 +28,17 @@ function TimePreference({
|
||||
|
||||
const menu = useMemo(
|
||||
() => ({
|
||||
items: menuItems,
|
||||
onClick: timeMenuItemOnChangeHandler,
|
||||
items: menuItems.map((item) => ({
|
||||
...item,
|
||||
onClick: timeMenuItemOnChangeHandler,
|
||||
})),
|
||||
}),
|
||||
[timeMenuItemOnChangeHandler],
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menu={menu}
|
||||
rootClassName="time-selection-menu"
|
||||
className="time-selection-target"
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button>
|
||||
<DropdownMenuSimple menu={menu} className="time-selection-menu">
|
||||
<Button className="time-selection-target">
|
||||
<div className="button-selected-text">
|
||||
<Globe size={14} />
|
||||
<Typography.Text className="selected-value">
|
||||
@@ -49,7 +47,7 @@ function TimePreference({
|
||||
</div>
|
||||
<ChevronDown size="md" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</DropdownMenuSimple>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Collapsed activity summary — one row that hides the underlying
|
||||
// thinking + tool-call steps. Reuses the same quiet treatment as
|
||||
// ThinkingStep / ToolCallStep so it sits flush in the assistant bubble.
|
||||
.activityGroup {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.activityHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--l3-foreground);
|
||||
user-select: none;
|
||||
transition: color 0.12s ease;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.sparkleIcon {
|
||||
flex-shrink: 0;
|
||||
color: var(--accent-primary);
|
||||
|
||||
&.iconPulsing {
|
||||
animation: activityGroupPulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes activityGroupPulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.55;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.activitySummary {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.toggleChevron {
|
||||
flex-shrink: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.activityBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2px 0 4px;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { ChevronDown, ChevronRight, Sparkles } from '@signozhq/icons';
|
||||
|
||||
import { formatTime } from 'utils/timeUtils';
|
||||
|
||||
import { StreamingToolCall } from '../../types';
|
||||
import ThinkingStep, { ThinkingContent, thinkingLabel } from '../ThinkingStep';
|
||||
import ToolCallStep, {
|
||||
getToolDisplayLabel,
|
||||
ToolCallContent,
|
||||
} from '../ToolCallStep';
|
||||
|
||||
import styles from './ActivityGroup.module.scss';
|
||||
|
||||
export type ActivityItem =
|
||||
| { id: string; kind: 'thinking'; content: string }
|
||||
| { id: string; kind: 'tool'; toolCall: StreamingToolCall };
|
||||
|
||||
interface ActivityGroupProps {
|
||||
items: ActivityItem[];
|
||||
/**
|
||||
* True only for the trailing activity group of an active stream — drives
|
||||
* the live "Working…" label and the elapsed-time tick (which re-stamps on
|
||||
* approval/clarification resume so wait time isn't counted).
|
||||
*/
|
||||
isLive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single-item groups get a step-specific summary so the user doesn't see a
|
||||
* pointless "Worked through 1 step". Multi-item groups roll up into the
|
||||
* generic "Working… / Worked through N steps" treatment.
|
||||
*/
|
||||
function buildSummary(
|
||||
items: ActivityItem[],
|
||||
isLive: boolean,
|
||||
elapsed: number,
|
||||
): string {
|
||||
if (items.length === 1) {
|
||||
const [only] = items;
|
||||
if (only.kind === 'thinking') {
|
||||
return thinkingLabel(isLive);
|
||||
}
|
||||
return getToolDisplayLabel(only.toolCall);
|
||||
}
|
||||
const stepLabel = `${items.length} steps`;
|
||||
if (!isLive) {
|
||||
return `Worked through ${stepLabel}`;
|
||||
}
|
||||
// Suppress the elapsed token until ≥ 1s — the first tick fires after
|
||||
// 1s anyway, and showing "0s" or "<1s" briefly adds noise.
|
||||
return elapsed >= 1000
|
||||
? `Working… · ${formatTime(elapsed / 1000)} · ${stepLabel}`
|
||||
: `Working… · ${stepLabel}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single collapsed summary row that hides a run of thinking + tool-call steps.
|
||||
* Expands to show each underlying step inline. Used for every activity row
|
||||
* (including single-item ones) so all "what the agent did" rows share a
|
||||
* consistent ✨-led visual contract.
|
||||
*/
|
||||
export default function ActivityGroup({
|
||||
items,
|
||||
isLive = false,
|
||||
}: ActivityGroupProps): JSX.Element {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
// Captures the moment this live phase started. Re-stamped on every
|
||||
// false→true transition so a stream that pauses on
|
||||
// approval/clarification and resumes doesn't roll the user's wait time
|
||||
// into the elapsed counter.
|
||||
const startedAtRef = useRef<number>(Date.now());
|
||||
const wasLiveRef = useRef<boolean>(isLive);
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLive && !wasLiveRef.current) {
|
||||
startedAtRef.current = Date.now();
|
||||
setElapsed(0);
|
||||
}
|
||||
wasLiveRef.current = isLive;
|
||||
|
||||
if (!isLive) {
|
||||
return undefined;
|
||||
}
|
||||
// Tick once per second — the display is integer-second precision, so
|
||||
// faster ticks would just re-render the bubble for no visible change.
|
||||
const id = window.setInterval(() => {
|
||||
setElapsed(Date.now() - startedAtRef.current);
|
||||
}, 1000);
|
||||
return (): void => window.clearInterval(id);
|
||||
}, [isLive]);
|
||||
|
||||
const summary = buildSummary(items, isLive, elapsed);
|
||||
const isSingle = items.length === 1;
|
||||
|
||||
const toggle = (): void => setExpanded((v) => !v);
|
||||
|
||||
return (
|
||||
<div className={styles.activityGroup}>
|
||||
<div className={styles.activityHeader} onClick={toggle}>
|
||||
<Sparkles
|
||||
size={12}
|
||||
className={cx(styles.sparkleIcon, { [styles.iconPulsing]: isLive })}
|
||||
/>
|
||||
<span className={styles.activitySummary}>{summary}</span>
|
||||
{expanded ? (
|
||||
<ChevronDown size={12} className={styles.toggleChevron} />
|
||||
) : (
|
||||
<ChevronRight size={12} className={styles.toggleChevron} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className={styles.activityBody}>
|
||||
{isSingle ? (
|
||||
// Single-item: the outer chevron already provides disclosure,
|
||||
// so render the underlying content directly instead of wrapping
|
||||
// it in a second collapsible step row.
|
||||
items[0].kind === 'thinking' ? (
|
||||
<ThinkingContent content={items[0].content} />
|
||||
) : (
|
||||
<ToolCallContent toolCall={items[0].toolCall} />
|
||||
)
|
||||
) : (
|
||||
items.map((item, i) => {
|
||||
// A thinking step is live only while it's the trailing item
|
||||
// in a trailing live group — once any later event (text or
|
||||
// tool) arrives, the pass is done.
|
||||
const isLastItem = i === items.length - 1;
|
||||
return item.kind === 'thinking' ? (
|
||||
<ThinkingStep
|
||||
key={item.id}
|
||||
content={item.content}
|
||||
isLive={isLive && isLastItem}
|
||||
/>
|
||||
) : (
|
||||
<ToolCallStep key={item.id} toolCall={item.toolCall} />
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from './ActivityGroup';
|
||||
export type { ActivityItem } from './ActivityGroup';
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import cx from 'classnames';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
@@ -9,11 +9,10 @@ import '../blocks';
|
||||
import { useVariant } from '../../VariantContext';
|
||||
import { Message, MessageBlock } from '../../types';
|
||||
import ActionsSection from '../ActionsSection';
|
||||
import ActivityGroup, { ActivityItem } from '../ActivityGroup';
|
||||
import { RichCodeBlock } from '../blocks';
|
||||
import { MessageContext } from '../MessageContext';
|
||||
import MessageFeedback from '../MessageFeedback';
|
||||
import ThinkingStep from '../ThinkingStep';
|
||||
import ToolCallStep from '../ToolCallStep';
|
||||
import UserMessageActions from '../UserMessageActions';
|
||||
|
||||
import styles from './MessageBubble.module.scss';
|
||||
@@ -40,38 +39,61 @@ function SmartPre({ children }: { children?: React.ReactNode }): JSX.Element {
|
||||
const MD_PLUGINS = [remarkGfm];
|
||||
const MD_COMPONENTS = { code: RichCodeBlock, pre: SmartPre };
|
||||
|
||||
/** Renders a single MessageBlock by type. */
|
||||
function renderBlock(block: MessageBlock, index: number): JSX.Element {
|
||||
switch (block.type) {
|
||||
case 'thinking':
|
||||
return <ThinkingStep key={index} content={block.content} />;
|
||||
case 'tool_call':
|
||||
// Blocks in a persisted message are always complete — done is always true.
|
||||
return (
|
||||
<ToolCallStep
|
||||
key={index}
|
||||
toolCall={{
|
||||
toolName: block.toolName,
|
||||
input: block.toolInput,
|
||||
result: block.result,
|
||||
done: true,
|
||||
displayText: block.displayText,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case 'text':
|
||||
default:
|
||||
return (
|
||||
<ReactMarkdown
|
||||
key={index}
|
||||
className={styles.markdown}
|
||||
remarkPlugins={MD_PLUGINS}
|
||||
components={MD_COMPONENTS}
|
||||
>
|
||||
{block.content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
type RenderGroup =
|
||||
| { kind: 'text'; id: string; content: string }
|
||||
| { kind: 'activity'; id: string; items: ActivityItem[] };
|
||||
|
||||
/**
|
||||
* Partition message blocks into render groups so consecutive thinking and
|
||||
* tool_call blocks collapse into a single ActivityGroup row. Text blocks
|
||||
* stand alone, mirroring the streaming view.
|
||||
*/
|
||||
function groupBlocks(blocks: MessageBlock[]): RenderGroup[] {
|
||||
const groups: RenderGroup[] = [];
|
||||
blocks.forEach((block, i) => {
|
||||
if (block.type === 'text') {
|
||||
groups.push({ kind: 'text', id: `text-${i}`, content: block.content });
|
||||
return;
|
||||
}
|
||||
const item: ActivityItem =
|
||||
block.type === 'thinking'
|
||||
? { id: `t-${i}`, kind: 'thinking', content: block.content }
|
||||
: {
|
||||
id: `c-${block.toolCallId}`,
|
||||
kind: 'tool',
|
||||
// Persisted blocks are always complete.
|
||||
toolCall: {
|
||||
toolName: block.toolName,
|
||||
input: block.toolInput,
|
||||
result: block.result,
|
||||
done: true,
|
||||
displayText: block.displayText,
|
||||
},
|
||||
};
|
||||
const last = groups[groups.length - 1];
|
||||
if (last?.kind === 'activity') {
|
||||
last.items.push(item);
|
||||
} else {
|
||||
groups.push({ kind: 'activity', id: `a-${i}`, items: [item] });
|
||||
}
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
function renderGroup(group: RenderGroup): JSX.Element {
|
||||
if (group.kind === 'text') {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
key={group.id}
|
||||
className={styles.markdown}
|
||||
remarkPlugins={MD_PLUGINS}
|
||||
components={MD_COMPONENTS}
|
||||
>
|
||||
{group.content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
return <ActivityGroup key={group.id} items={group.items} />;
|
||||
}
|
||||
|
||||
interface MessageBubbleProps {
|
||||
@@ -90,6 +112,14 @@ export default function MessageBubble({
|
||||
const isUser = message.role === 'user';
|
||||
const hasBlocks = !isUser && message.blocks && message.blocks.length > 0;
|
||||
|
||||
// Recompute groups only when the blocks array identity changes — store
|
||||
// updates that don't touch this message's blocks should not re-render the
|
||||
// underlying ThinkingStep/ToolCallStep children.
|
||||
const groups = useMemo(
|
||||
() => (hasBlocks ? groupBlocks(message.blocks!) : []),
|
||||
[hasBlocks, message.blocks],
|
||||
);
|
||||
|
||||
const messageClass = cx(
|
||||
styles.message,
|
||||
isUser ? styles.user : styles.assistant,
|
||||
@@ -128,8 +158,7 @@ export default function MessageBubble({
|
||||
<p className={styles.text}>{message.content}</p>
|
||||
) : hasBlocks ? (
|
||||
<MessageContext.Provider value={{ messageId: message.id }}>
|
||||
{/* eslint-disable-next-line react/no-array-index-key */}
|
||||
{message.blocks!.map((block, i) => renderBlock(block, i))}
|
||||
{groups.map((g) => renderGroup(g))}
|
||||
</MessageContext.Provider>
|
||||
) : (
|
||||
<MessageContext.Provider value={{ messageId: message.id }}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import cx from 'classnames';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
@@ -10,11 +10,10 @@ import type {
|
||||
|
||||
import { useVariant } from '../../VariantContext';
|
||||
import { StreamingEventItem } from '../../types';
|
||||
import ActivityGroup, { ActivityItem } from '../ActivityGroup';
|
||||
import ApprovalCard from '../ApprovalCard';
|
||||
import { RichCodeBlock } from '../blocks';
|
||||
import ClarificationForm from '../ClarificationForm';
|
||||
import ThinkingStep from '../ThinkingStep';
|
||||
import ToolCallStep from '../ToolCallStep';
|
||||
|
||||
import messageStyles from '../MessageBubble/MessageBubble.module.scss';
|
||||
import styles from './StreamingMessage.module.scss';
|
||||
@@ -33,6 +32,59 @@ function SmartPre({ children }: { children?: React.ReactNode }): JSX.Element {
|
||||
const MD_PLUGINS = [remarkGfm];
|
||||
const MD_COMPONENTS = { code: RichCodeBlock, pre: SmartPre };
|
||||
|
||||
type RenderGroup =
|
||||
| { kind: 'text'; id: string; content: string }
|
||||
| {
|
||||
kind: 'activity';
|
||||
id: string;
|
||||
items: ActivityItem[];
|
||||
isTrailing: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Partition the streaming event timeline into render groups: runs of
|
||||
* consecutive thinking/tool events fold into a single activity group, text
|
||||
* events stay standalone. The last group is flagged as trailing so the
|
||||
* caller can drive a "live" indicator on it.
|
||||
*
|
||||
* Invariant relied on by the ActivityGroup elapsed-time timer: once a
|
||||
* group exists at a given array index, later events only extend its
|
||||
* `items` — they never shrink the array or re-key existing groups. That
|
||||
* keeps each ActivityGroup React instance stable across re-renders so the
|
||||
* timer's `wasLive` → `isLive` re-stamp captures the right transition.
|
||||
* The id fields below piggyback on that invariant: each event's position in
|
||||
* `events` is stable, so the derived id stays stable across re-renders.
|
||||
*/
|
||||
function groupStreamingEvents(events: StreamingEventItem[]): RenderGroup[] {
|
||||
const groups: RenderGroup[] = [];
|
||||
events.forEach((event, i) => {
|
||||
if (event.kind === 'text') {
|
||||
groups.push({ kind: 'text', id: `text-${i}`, content: event.content });
|
||||
return;
|
||||
}
|
||||
const item: ActivityItem =
|
||||
event.kind === 'thinking'
|
||||
? { id: `t-${i}`, kind: 'thinking', content: event.content }
|
||||
: { id: `c-${i}`, kind: 'tool', toolCall: event.toolCall };
|
||||
const last = groups[groups.length - 1];
|
||||
if (last?.kind === 'activity') {
|
||||
last.items.push(item);
|
||||
} else {
|
||||
groups.push({
|
||||
kind: 'activity',
|
||||
id: `a-${i}`,
|
||||
items: [item],
|
||||
isTrailing: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
const last = groups[groups.length - 1];
|
||||
if (last?.kind === 'activity') {
|
||||
last.isTrailing = true;
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/** Human-readable labels for execution status codes shown before any events arrive. */
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
queued: 'Queued…',
|
||||
@@ -79,6 +131,11 @@ export default function StreamingMessage({
|
||||
[messageStyles.compact]: isCompact,
|
||||
});
|
||||
|
||||
// Recompute groups only when the events array identity changes. The
|
||||
// streaming reducer pushes new entries into the same array reference
|
||||
// once per tick, so this naturally invalidates as events arrive.
|
||||
const groups = useMemo(() => groupStreamingEvents(events), [events]);
|
||||
|
||||
return (
|
||||
<div className={messageClass}>
|
||||
<div className={messageStyles.bubble}>
|
||||
@@ -88,27 +145,28 @@ export default function StreamingMessage({
|
||||
)}
|
||||
{isEmpty && !statusLabel && <TypingDots />}
|
||||
|
||||
{/* eslint-disable react/no-array-index-key */}
|
||||
{/* Events rendered in arrival order: text, thinking, and tool calls interleaved */}
|
||||
{events.map((event, i) => {
|
||||
if (event.kind === 'tool') {
|
||||
return <ToolCallStep key={i} toolCall={event.toolCall} />;
|
||||
}
|
||||
if (event.kind === 'thinking') {
|
||||
return <ThinkingStep key={i} content={event.content} />;
|
||||
{/* Runs of consecutive thinking + tool events collapse into a
|
||||
single ActivityGroup; text events render inline between
|
||||
them. The trailing group is "live" while streaming is
|
||||
active and not blocked on the user. */}
|
||||
{groups.map((group) => {
|
||||
if (group.kind === 'text') {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
key={group.id}
|
||||
className={messageStyles.markdown}
|
||||
remarkPlugins={MD_PLUGINS}
|
||||
components={MD_COMPONENTS}
|
||||
>
|
||||
{group.content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
const groupIsLive = group.isTrailing && !isWaitingOnUser;
|
||||
return (
|
||||
<ReactMarkdown
|
||||
key={i}
|
||||
className={messageStyles.markdown}
|
||||
remarkPlugins={MD_PLUGINS}
|
||||
components={MD_COMPONENTS}
|
||||
>
|
||||
{event.content}
|
||||
</ReactMarkdown>
|
||||
<ActivityGroup key={group.id} items={group.items} isLive={groupIsLive} />
|
||||
);
|
||||
})}
|
||||
{/* eslint-enable react/no-array-index-key */}
|
||||
|
||||
{/* While events are still streaming, append the typing dots so the
|
||||
user has a clear "more is coming" signal. Hidden when the agent
|
||||
|
||||
@@ -5,11 +5,31 @@ import styles from './ThinkingStep.module.scss';
|
||||
|
||||
interface ThinkingStepProps {
|
||||
content: string;
|
||||
/**
|
||||
* When false, label reads "Thought for a few seconds" — intentionally
|
||||
* vague because the API doesn't persist precise timing, so showing
|
||||
* seconds would be inconsistent between fresh and reloaded threads.
|
||||
*/
|
||||
isLive?: boolean;
|
||||
}
|
||||
|
||||
/** Body of a thinking step — extracted so ActivityGroup can render it directly. */
|
||||
export function ThinkingContent({ content }: { content: string }): JSX.Element {
|
||||
return (
|
||||
<div className={styles.body}>
|
||||
<p className={styles.content}>{content}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function thinkingLabel(isLive: boolean): string {
|
||||
return isLive ? 'Thinking…' : 'Thought for a few seconds';
|
||||
}
|
||||
|
||||
/** Collapsible thinking row — chevron + label, content in the expanded body. */
|
||||
export default function ThinkingStep({
|
||||
content,
|
||||
isLive = false,
|
||||
}: ThinkingStepProps): JSX.Element {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
@@ -23,14 +43,10 @@ export default function ThinkingStep({
|
||||
) : (
|
||||
<ChevronRight size={12} className={styles.chevron} />
|
||||
)}
|
||||
<span className={styles.label}>Thinking</span>
|
||||
<span className={styles.label}>{thinkingLabel(isLive)}</span>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className={styles.body}>
|
||||
<p className={styles.content}>{content}</p>
|
||||
</div>
|
||||
)}
|
||||
{expanded && <ThinkingContent content={content} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,24 +10,58 @@ interface ToolCallStepProps {
|
||||
toolCall: StreamingToolCall;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-supplied `displayText` is the human-friendly title the backend
|
||||
* wants surfaced. Falls back to a derived label
|
||||
* ("signoz_get_dashboard" → "Get Dashboard") when missing.
|
||||
*/
|
||||
export function getToolDisplayLabel(toolCall: StreamingToolCall): string {
|
||||
const { toolName, displayText } = toolCall;
|
||||
if (displayText && displayText.trim().length > 0) {
|
||||
return displayText;
|
||||
}
|
||||
return toolName
|
||||
.replace(/^[a-z]+_/, '') // strip prefix like "signoz_"
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
/** Body of a tool-call step — extracted so ActivityGroup can render it directly. */
|
||||
export function ToolCallContent({
|
||||
toolCall,
|
||||
}: {
|
||||
toolCall: StreamingToolCall;
|
||||
}): JSX.Element {
|
||||
const { toolName, input, result, done } = toolCall;
|
||||
return (
|
||||
<div className={styles.body}>
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Tool</span>
|
||||
<span className={styles.toolName}>{toolName}</span>
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Input</span>
|
||||
<pre className={styles.json}>{JSON.stringify(input, null, 2)}</pre>
|
||||
</div>
|
||||
{done && result !== undefined && (
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Output</span>
|
||||
<pre className={styles.json}>
|
||||
{typeof result === 'string' ? result : JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Collapsible tool-call row — chevron + label, in/out detail in the body. */
|
||||
export default function ToolCallStep({
|
||||
toolCall,
|
||||
}: ToolCallStepProps): JSX.Element {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const { toolName, input, result, done, displayText } = toolCall;
|
||||
|
||||
// Prefer the server-supplied `displayText` from `ToolCallEventDTO` —
|
||||
// it's the human-friendly title the backend wants surfaced. Fall back
|
||||
// to a derived label ("signoz_get_dashboard" → "Get Dashboard") when
|
||||
// the field is empty / null / missing.
|
||||
const label =
|
||||
displayText && displayText.trim().length > 0
|
||||
? displayText
|
||||
: toolName
|
||||
.replace(/^[a-z]+_/, '') // strip prefix like "signoz_"
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
const { done } = toolCall;
|
||||
const label = getToolDisplayLabel(toolCall);
|
||||
|
||||
const toggle = (): void => setExpanded((v) => !v);
|
||||
|
||||
@@ -44,26 +78,7 @@ export default function ToolCallStep({
|
||||
<span className={styles.label}>{label}</span>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className={styles.body}>
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Tool</span>
|
||||
<span className={styles.toolName}>{toolName}</span>
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Input</span>
|
||||
<pre className={styles.json}>{JSON.stringify(input, null, 2)}</pre>
|
||||
</div>
|
||||
{done && result !== undefined && (
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Output</span>
|
||||
<pre className={styles.json}>
|
||||
{typeof result === 'string' ? result : JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{expanded && <ToolCallContent toolCall={toolCall} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,6 @@ function ChartPreview({
|
||||
const renderQBChartPreview = (): JSX.Element => (
|
||||
<ChartPreviewComponent
|
||||
headline={headline}
|
||||
name=""
|
||||
query={stagedQuery}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
alertDef={alertDef}
|
||||
@@ -107,7 +106,6 @@ function ChartPreview({
|
||||
const renderPromAndChQueryChartPreview = (): JSX.Element => (
|
||||
<ChartPreviewComponent
|
||||
headline={headline}
|
||||
name="Chart Preview"
|
||||
query={stagedQuery}
|
||||
alertDef={alertDef}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
|
||||
@@ -17,7 +17,6 @@ import { CreateAlertProvider } from '../../context';
|
||||
import ChartPreview from '../ChartPreview/ChartPreview';
|
||||
|
||||
const REQUESTS_PER_SEC = 'requests/sec';
|
||||
const CHART_PREVIEW_NAME = 'Chart Preview';
|
||||
const QUERY_TYPE_TEST_ID = 'query-type';
|
||||
const GRAPH_TYPE_TEST_ID = 'graph-type';
|
||||
const CHART_PREVIEW_COMPONENT_TEST_ID = 'chart-preview-component';
|
||||
@@ -34,7 +33,6 @@ jest.mock(
|
||||
return (
|
||||
<div data-testid={CHART_PREVIEW_COMPONENT_TEST_ID}>
|
||||
<div data-testid="headline">{props.headline}</div>
|
||||
<div data-testid="name">{props.name}</div>
|
||||
<div data-testid={QUERY_TYPE_TEST_ID}>{props.query?.queryType}</div>
|
||||
<div data-testid="selected-interval">
|
||||
{props.selectedInterval?.startTime}
|
||||
@@ -175,12 +173,6 @@ describe('ChartPreview', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('renders QueryBuilder chart preview with empty name when query type is QUERY_BUILDER', () => {
|
||||
renderChartPreview();
|
||||
|
||||
expect(screen.getByTestId('name')).toHaveTextContent('');
|
||||
});
|
||||
|
||||
it('renders QueryBuilder chart preview with correct props', () => {
|
||||
renderChartPreview();
|
||||
|
||||
@@ -191,7 +183,6 @@ describe('ChartPreview', () => {
|
||||
expect(screen.getByTestId(GRAPH_TYPE_TEST_ID)).toHaveTextContent(
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
);
|
||||
expect(screen.getByTestId('name')).toHaveTextContent('');
|
||||
expect(screen.getByTestId('headline')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('selected-interval')).toBeInTheDocument();
|
||||
});
|
||||
@@ -214,7 +205,6 @@ describe('ChartPreview', () => {
|
||||
expect(
|
||||
screen.getByTestId(CHART_PREVIEW_COMPONENT_TEST_ID),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('name')).toHaveTextContent(CHART_PREVIEW_NAME);
|
||||
expect(screen.getByTestId(QUERY_TYPE_TEST_ID)).toHaveTextContent(
|
||||
EQueryType.PROM,
|
||||
);
|
||||
@@ -238,7 +228,6 @@ describe('ChartPreview', () => {
|
||||
expect(
|
||||
screen.getByTestId(CHART_PREVIEW_COMPONENT_TEST_ID),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('name')).toHaveTextContent(CHART_PREVIEW_NAME);
|
||||
expect(screen.getByTestId(QUERY_TYPE_TEST_ID)).toHaveTextContent(
|
||||
EQueryType.CLICKHOUSE,
|
||||
);
|
||||
|
||||
@@ -11,8 +11,13 @@ import {
|
||||
} from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@signozhq/ui/dropdown-menu';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Dropdown, Skeleton } from 'antd';
|
||||
import { Skeleton } from 'antd';
|
||||
import {
|
||||
RenderErrorResponseDTO,
|
||||
ZeustypesHostDTO,
|
||||
@@ -200,10 +205,15 @@ export default function CustomDomainSettings(): JSX.Element {
|
||||
!workspaceName ? 'workspace-name-hidden' : ''
|
||||
}`}
|
||||
>
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
disabled={isFetchingHosts}
|
||||
dropdownRender={(): JSX.Element => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="link" color="none" disabled={isFetchingHosts}>
|
||||
<Link2 size={12} />
|
||||
<span>{stripProtocol(activeHost?.url ?? '')}</span>
|
||||
<ChevronDown size={12} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<div className="workspace-url-dropdown">
|
||||
<span className="workspace-url-dropdown-header">
|
||||
All Workspace URLs
|
||||
@@ -236,14 +246,8 @@ export default function CustomDomainSettings(): JSX.Element {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Button variant="link" color="none">
|
||||
<Link2 size={12} />
|
||||
<span>{stripProtocol(activeHost?.url ?? '')}</span>
|
||||
<ChevronDown size={12} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<span className="custom-domain-card-meta-timezone">
|
||||
<Clock size={11} />
|
||||
{timezone.offset}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GetHosts200 } from 'api/generated/services/sigNoz.schemas';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
@@ -142,12 +143,13 @@ describe('CustomDomainSettings', () => {
|
||||
});
|
||||
|
||||
it('shows all workspace URLs as links in the dropdown', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<CustomDomainSettings />);
|
||||
|
||||
await screen.findByText(/custom-host\.test\.cloud/i);
|
||||
|
||||
// Open the URL dropdown
|
||||
fireEvent.click(
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /custom-host\.test\.cloud/i }),
|
||||
);
|
||||
|
||||
|
||||
@@ -17,10 +17,11 @@ import { getTimeRange } from 'utils/getTimeRange';
|
||||
import BarChart from '../../charts/BarChart/BarChart';
|
||||
import ChartManager from '../../components/ChartManager/ChartManager';
|
||||
import { usePanelContextMenu } from '../../hooks/usePanelContextMenu';
|
||||
import { prepareBarPanelConfig, prepareBarPanelData } from './utils';
|
||||
import { prepareBarPanelConfig } from './utils';
|
||||
|
||||
import '../Panel.styles.scss';
|
||||
import TooltipFooter from '../components/TooltipFooter';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
|
||||
function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
const {
|
||||
@@ -99,7 +100,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
if (!queryResponse?.data?.payload) {
|
||||
return [];
|
||||
}
|
||||
return prepareBarPanelData(queryResponse?.data?.payload);
|
||||
return prepareChartData(queryResponse?.data?.payload);
|
||||
}, [queryResponse?.data?.payload]);
|
||||
|
||||
const layoutChildren = useMemo(() => {
|
||||
|
||||
@@ -11,21 +11,10 @@ import { get } from 'lodash-es';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { AlignedData } from 'uplot';
|
||||
|
||||
import { PanelMode } from '../types';
|
||||
import { fillMissingXAxisTimestamps, getXAxisTimestamps } from '../utils';
|
||||
import { buildBaseConfig } from '../utils/baseConfigBuilder';
|
||||
|
||||
export function prepareBarPanelData(
|
||||
apiResponse: MetricRangePayloadProps,
|
||||
): AlignedData {
|
||||
const seriesList = apiResponse?.data?.result || [];
|
||||
const timestampArr = getXAxisTimestamps(seriesList);
|
||||
const yAxisValuesArr = fillMissingXAxisTimestamps(timestampArr, seriesList);
|
||||
return [timestampArr, ...yAxisValuesArr];
|
||||
}
|
||||
|
||||
export function prepareBarPanelConfig({
|
||||
widget,
|
||||
isDarkMode,
|
||||
|
||||
@@ -17,10 +17,11 @@ import { useTimezone } from 'providers/Timezone';
|
||||
import uPlot from 'uplot';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { prepareChartData, prepareUPlotConfig } from '../TimeSeriesPanel/utils';
|
||||
import { prepareUPlotConfig } from '../TimeSeriesPanel/utils';
|
||||
|
||||
import '../Panel.styles.scss';
|
||||
import TooltipFooter from '../components/TooltipFooter';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
|
||||
function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
const {
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { PanelMode } from '../../types';
|
||||
import { prepareChartData, prepareUPlotConfig } from '../utils';
|
||||
import { prepareUPlotConfig } from '../utils';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils',
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { ExecStats } from 'api/v5/v5';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
fillMissingXAxisTimestamps,
|
||||
getXAxisTimestamps,
|
||||
} from 'container/DashboardContainer/visualization/panels/utils';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
@@ -15,42 +11,15 @@ import {
|
||||
LineStyle,
|
||||
} from 'lib/uPlotV2/config/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { isInvalidPlotValue } from 'lib/uPlotV2/utils/dataUtils';
|
||||
import { hasSingleVisiblePoint } from 'lib/uPlotV2/utils/dataUtils';
|
||||
import get from 'lodash-es/get';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
|
||||
import { PanelMode } from '../types';
|
||||
import { buildBaseConfig } from '../utils/baseConfigBuilder';
|
||||
|
||||
export const prepareChartData = (
|
||||
apiResponse: MetricRangePayloadProps,
|
||||
): uPlot.AlignedData => {
|
||||
const seriesList = apiResponse?.data?.result || [];
|
||||
const timestampArr = getXAxisTimestamps(seriesList);
|
||||
const yAxisValuesArr = fillMissingXAxisTimestamps(timestampArr, seriesList);
|
||||
|
||||
return [timestampArr, ...yAxisValuesArr];
|
||||
};
|
||||
|
||||
function hasSingleVisiblePointForSeries(series: QueryData): boolean {
|
||||
const rawValues = series.values ?? [];
|
||||
let validPointCount = 0;
|
||||
|
||||
for (const [, rawValue] of rawValues) {
|
||||
if (!isInvalidPlotValue(rawValue)) {
|
||||
validPointCount += 1;
|
||||
if (validPointCount > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export const prepareUPlotConfig = ({
|
||||
widget,
|
||||
isDarkMode,
|
||||
@@ -107,7 +76,7 @@ export const prepareUPlotConfig = ({
|
||||
}
|
||||
|
||||
apiResponse.data.result.forEach((series) => {
|
||||
const hasSingleValidPoint = hasSingleVisiblePointForSeries(series);
|
||||
const hasSingleValidPoint = hasSingleVisiblePoint(series.values);
|
||||
const baseLabelName = getLabelName(
|
||||
series.metric,
|
||||
series.queryName || '', // query
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { CloudDownload } from '@signozhq/icons';
|
||||
import { Button, Dropdown, MenuProps, Flex } from 'antd';
|
||||
import { DropdownMenuSimple, type MenuProps } from '@signozhq/ui/dropdown-menu';
|
||||
import { Button, Flex } from 'antd';
|
||||
import { unparse } from 'papaparse';
|
||||
|
||||
import { DownloadProps } from './Download.types';
|
||||
@@ -67,7 +68,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown menu={menu} trigger={['click']}>
|
||||
<DropdownMenuSimple menu={menu}>
|
||||
<Button
|
||||
className="download-button"
|
||||
loading={isLoading || isDownloading}
|
||||
@@ -79,7 +80,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
|
||||
Download
|
||||
</Flex>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</DropdownMenuSimple>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import {
|
||||
AlertChartPanelType,
|
||||
buildAlertChartConfig,
|
||||
buildChartId,
|
||||
} from './utils';
|
||||
|
||||
// Panel types that render through the UPlotConfigBuilder pipeline.
|
||||
// To support a new modern-chart panel type, add an entry here and extend
|
||||
// `AlertChartPanelType` / `buildAlertChartConfig` to handle its series setup.
|
||||
const SUPPORTED_CHARTS: Record<
|
||||
AlertChartPanelType,
|
||||
typeof TimeSeries | typeof BarChart
|
||||
> = {
|
||||
[PANEL_TYPES.TIME_SERIES]: TimeSeries,
|
||||
[PANEL_TYPES.BAR]: BarChart,
|
||||
};
|
||||
|
||||
const isSupportedPanelType = (
|
||||
panelType: PANEL_TYPES,
|
||||
): panelType is AlertChartPanelType => panelType in SUPPORTED_CHARTS;
|
||||
|
||||
export interface ChartContentProps {
|
||||
panelType: PANEL_TYPES;
|
||||
alertId?: string;
|
||||
query: Query;
|
||||
apiResponse?: MetricRangePayloadProps;
|
||||
data: uPlot.AlignedData;
|
||||
thresholds: ThresholdProps[];
|
||||
yAxisUnit: string;
|
||||
legendPosition: LegendPosition;
|
||||
isDarkMode: boolean;
|
||||
timezone: Timezone;
|
||||
width: number;
|
||||
height: number;
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
}
|
||||
|
||||
export default function ChartContent({
|
||||
panelType,
|
||||
alertId,
|
||||
query,
|
||||
thresholds,
|
||||
apiResponse,
|
||||
data,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
onDragSelect,
|
||||
width,
|
||||
height,
|
||||
legendPosition,
|
||||
}: ChartContentProps): JSX.Element | null {
|
||||
const supported = isSupportedPanelType(panelType);
|
||||
|
||||
const config = useMemo(
|
||||
() =>
|
||||
buildAlertChartConfig({
|
||||
id: buildChartId(alertId),
|
||||
panelType: panelType as AlertChartPanelType,
|
||||
query,
|
||||
thresholds,
|
||||
apiResponse,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
onDragSelect,
|
||||
}),
|
||||
[
|
||||
alertId,
|
||||
panelType,
|
||||
query,
|
||||
thresholds,
|
||||
apiResponse,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
onDragSelect,
|
||||
],
|
||||
);
|
||||
|
||||
if (!supported) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Component = SUPPORTED_CHARTS[panelType];
|
||||
|
||||
return (
|
||||
<Component
|
||||
config={config}
|
||||
data={data}
|
||||
width={width}
|
||||
height={height}
|
||||
legendConfig={{ position: legendPosition }}
|
||||
canPinTooltip
|
||||
yAxisUnit={yAxisUnit}
|
||||
timezone={timezone}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -15,8 +15,6 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
|
||||
import { INITIAL_CRITICAL_THRESHOLD } from 'container/CreateAlertV2/context/constants';
|
||||
import { Threshold } from 'container/CreateAlertV2/context/types';
|
||||
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
@@ -32,8 +30,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import history from 'lib/history';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
@@ -41,24 +38,27 @@ import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
import { LegendPosition } from 'types/api/dashboard/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import uPlot from 'uplot';
|
||||
import { getGraphType } from 'utils/getGraphType';
|
||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { AlertDetectionTypes } from '..';
|
||||
import ChartContent from './ChartContent';
|
||||
import { ChartContainer } from './styles';
|
||||
import { getThresholds } from './utils';
|
||||
|
||||
import './ChartPreview.styles.scss';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
|
||||
// Height reserved for the `.chart-preview-header` strip rendered above the chart.
|
||||
const CHART_PREVIEW_HEADER_HEIGHT = 48;
|
||||
const CHART_PREVIEW_CONTAINER_PADDING = 16;
|
||||
|
||||
export interface ChartPreviewProps {
|
||||
name: string;
|
||||
query: Query | null;
|
||||
graphType?: PANEL_TYPES;
|
||||
selectedTime?: timePreferenceType;
|
||||
@@ -77,7 +77,6 @@ export interface ChartPreviewProps {
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function ChartPreview({
|
||||
name,
|
||||
query,
|
||||
graphType = PANEL_TYPES.TIME_SERIES,
|
||||
selectedTime = 'GLOBAL_TIME',
|
||||
@@ -113,14 +112,6 @@ function ChartPreview({
|
||||
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]);
|
||||
const legendScrollPositionRef = useRef<{
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
}>({
|
||||
scrollTop: 0,
|
||||
scrollLeft: 0,
|
||||
});
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const {
|
||||
@@ -219,18 +210,6 @@ function ChartPreview({
|
||||
setMaxTimeScale(endTime);
|
||||
}, [maxTime, minTime, globalSelectedInterval, queryResponse, setQueryStatus]);
|
||||
|
||||
// Initialize graph visibility from localStorage
|
||||
useEffect(() => {
|
||||
if (queryResponse?.data?.payload?.data?.result) {
|
||||
const { graphVisibilityStates: localStoredVisibilityState } =
|
||||
getLocalStorageGraphVisibilityState({
|
||||
apiResponse: queryResponse.data.payload.data.result,
|
||||
name: 'alert-chart-preview',
|
||||
});
|
||||
setGraphVisibility(localStoredVisibilityState);
|
||||
}
|
||||
}, [queryResponse?.data?.payload?.data?.result]);
|
||||
|
||||
if (queryResponse.data && graphType === PANEL_TYPES.BAR) {
|
||||
const sortedSeriesData = getSortedSeriesData(
|
||||
queryResponse.data?.payload.data.result,
|
||||
@@ -288,62 +267,17 @@ function ChartPreview({
|
||||
return LegendPosition.RIGHT;
|
||||
}, [queryResponse?.data?.payload?.data?.result?.length, showSideLegend]);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
id: 'alert_legend_widget',
|
||||
yAxisUnit,
|
||||
apiResponse: queryResponse?.data?.payload,
|
||||
dimensions: {
|
||||
height: containerDimensions?.height ? containerDimensions.height - 48 : 0,
|
||||
width: containerDimensions?.width,
|
||||
},
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
thresholds: getThresholds(thresholds, t, optionName, yAxisUnit),
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
panelType: graphType,
|
||||
tzDate: (timestamp: number) =>
|
||||
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||
timezone: timezone.value,
|
||||
currentQuery,
|
||||
query: query || currentQuery,
|
||||
graphsVisibilityStates: graphVisibility,
|
||||
setGraphsVisibilityStates: setGraphVisibility,
|
||||
enhancedLegend: true,
|
||||
legendPosition,
|
||||
legendScrollPosition: legendScrollPositionRef.current,
|
||||
setLegendScrollPosition: (position: {
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
}) => {
|
||||
legendScrollPositionRef.current = position;
|
||||
},
|
||||
}),
|
||||
[
|
||||
yAxisUnit,
|
||||
queryResponse?.data?.payload,
|
||||
containerDimensions,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
thresholds,
|
||||
t,
|
||||
optionName,
|
||||
graphType,
|
||||
timezone.value,
|
||||
currentQuery,
|
||||
query,
|
||||
graphVisibility,
|
||||
legendPosition,
|
||||
],
|
||||
const resolvedThresholds = useMemo(
|
||||
() => getThresholds(thresholds, t, optionName, yAxisUnit),
|
||||
[thresholds, t, optionName, yAxisUnit],
|
||||
);
|
||||
|
||||
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
||||
const chartData = useMemo(() => {
|
||||
if (!queryResponse?.data?.payload) {
|
||||
return [];
|
||||
}
|
||||
return prepareChartData(queryResponse?.data?.payload);
|
||||
}, [queryResponse?.data?.payload]);
|
||||
|
||||
const hasResultData = !!queryResponse?.data?.payload?.data?.result?.length;
|
||||
|
||||
@@ -361,6 +295,14 @@ function ChartPreview({
|
||||
?.active || false;
|
||||
|
||||
const isWarning = !isEmpty(queryResponse.data?.warning);
|
||||
|
||||
const chartWidth = containerDimensions?.width
|
||||
? containerDimensions.width - CHART_PREVIEW_CONTAINER_PADDING
|
||||
: 0;
|
||||
const chartHeight = containerDimensions?.height
|
||||
? containerDimensions.height - CHART_PREVIEW_HEADER_HEIGHT
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="alert-chart-container" ref={graphRef}>
|
||||
<ChartContainer>
|
||||
@@ -384,16 +326,22 @@ function ChartPreview({
|
||||
)}
|
||||
|
||||
{chartDataAvailable && !isAnomalyDetectionAlert && (
|
||||
<GridPanelSwitch
|
||||
options={options}
|
||||
<ChartContent
|
||||
panelType={graphType}
|
||||
alertId={alertDef?.id}
|
||||
query={query || currentQuery}
|
||||
apiResponse={queryResponse.data?.payload}
|
||||
data={chartData}
|
||||
name={name || 'Chart Preview'}
|
||||
panelData={
|
||||
queryResponse.data?.payload?.data?.newResult?.data?.result || []
|
||||
}
|
||||
query={query || initialQueriesMap.metrics}
|
||||
thresholds={resolvedThresholds}
|
||||
yAxisUnit={yAxisUnit}
|
||||
legendPosition={legendPosition}
|
||||
isDarkMode={isDarkMode}
|
||||
timezone={timezone}
|
||||
width={chartWidth}
|
||||
height={chartHeight}
|
||||
minTimeScale={minTimeScale}
|
||||
maxTimeScale={maxTimeScale}
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { ExecStats } from 'api/v5/v5';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Threshold } from 'container/CreateAlertV2/context/types';
|
||||
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import {
|
||||
BooleanFormats,
|
||||
@@ -11,6 +15,20 @@ import {
|
||||
TimeFormats,
|
||||
} from 'container/NewWidget/RightContainer/types';
|
||||
import { TFunction } from 'i18next';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import {
|
||||
DrawStyle,
|
||||
FillMode,
|
||||
LineInterpolation,
|
||||
LineStyle,
|
||||
} from 'lib/uPlotV2/config/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { hasSingleVisiblePoint } from 'lib/uPlotV2/utils/dataUtils';
|
||||
import { get } from 'lodash-es';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
dataFormatConfig,
|
||||
@@ -20,6 +38,8 @@ import {
|
||||
timeUnitsConfig,
|
||||
} from './config';
|
||||
|
||||
const CHART_ID_PREFIX = 'alert_legend_widget';
|
||||
|
||||
export function covertIntoDataFormats({
|
||||
value,
|
||||
sourceUnit,
|
||||
@@ -142,3 +162,110 @@ export const getThresholds = (
|
||||
});
|
||||
return thresholdsToReturn;
|
||||
};
|
||||
|
||||
export type AlertChartPanelType = PANEL_TYPES.TIME_SERIES | PANEL_TYPES.BAR;
|
||||
|
||||
export interface BuildAlertChartConfigParams {
|
||||
id: string;
|
||||
panelType: AlertChartPanelType;
|
||||
query: Query;
|
||||
thresholds: ThresholdProps[];
|
||||
apiResponse?: MetricRangePayloadProps;
|
||||
yAxisUnit?: string;
|
||||
isDarkMode: boolean;
|
||||
timezone: Timezone;
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
onDragSelect: (startTime: number, endTime: number) => void;
|
||||
onClick?: OnClickPluginOpts['onClick'];
|
||||
}
|
||||
|
||||
export const buildAlertChartConfig = ({
|
||||
id,
|
||||
panelType,
|
||||
query,
|
||||
thresholds,
|
||||
apiResponse,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
onDragSelect,
|
||||
onClick,
|
||||
}: BuildAlertChartConfigParams): UPlotConfigBuilder => {
|
||||
const stepIntervals: ExecStats['stepIntervals'] = get(
|
||||
apiResponse,
|
||||
'data.newResult.meta.stepIntervals',
|
||||
{},
|
||||
);
|
||||
const stepIntervalValues = Object.values(stepIntervals);
|
||||
const minStepInterval = stepIntervalValues.length
|
||||
? Math.min(...stepIntervalValues)
|
||||
: undefined;
|
||||
|
||||
const builder = buildBaseConfig({
|
||||
id,
|
||||
panelType,
|
||||
panelMode: PanelMode.DASHBOARD_VIEW,
|
||||
thresholds,
|
||||
apiResponse,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
stepInterval: minStepInterval,
|
||||
onDragSelect,
|
||||
onClick,
|
||||
});
|
||||
|
||||
const seriesList = apiResponse?.data?.result;
|
||||
if (!seriesList?.length) {
|
||||
return builder;
|
||||
}
|
||||
|
||||
const isBar = panelType === PANEL_TYPES.BAR;
|
||||
|
||||
seriesList.forEach((series) => {
|
||||
const baseLabelName = getLabelName(
|
||||
series.metric,
|
||||
series.queryName || '',
|
||||
series.legend || '',
|
||||
);
|
||||
const label = query ? getLegend(series, query, baseLabelName) : baseLabelName;
|
||||
|
||||
if (isBar) {
|
||||
builder.addSeries({
|
||||
scaleKey: 'y',
|
||||
drawStyle: DrawStyle.Bar,
|
||||
label,
|
||||
colorMapping: {},
|
||||
isDarkMode,
|
||||
stepInterval: get(stepIntervals, series.queryName, undefined),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const hasSingleValidPoint = hasSingleVisiblePoint(series.values);
|
||||
builder.addSeries({
|
||||
scaleKey: 'y',
|
||||
drawStyle: hasSingleValidPoint ? DrawStyle.Points : DrawStyle.Line,
|
||||
label,
|
||||
colorMapping: {},
|
||||
spanGaps: true,
|
||||
lineStyle: LineStyle.Solid,
|
||||
lineInterpolation: LineInterpolation.Spline,
|
||||
showPoints: hasSingleValidPoint,
|
||||
pointSize: 5,
|
||||
fillMode: FillMode.None,
|
||||
isDarkMode,
|
||||
metric: series.metric,
|
||||
});
|
||||
});
|
||||
|
||||
return builder;
|
||||
};
|
||||
|
||||
export const buildChartId = (id?: string): string =>
|
||||
id ? `${CHART_ID_PREFIX}_${id}` : CHART_ID_PREFIX;
|
||||
|
||||
@@ -719,7 +719,6 @@ function FormAlertRules({
|
||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
/>
|
||||
}
|
||||
name=""
|
||||
query={stagedQuery}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
alertDef={alertDef}
|
||||
@@ -739,7 +738,6 @@ function FormAlertRules({
|
||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
/>
|
||||
}
|
||||
name="Chart Preview"
|
||||
query={stagedQuery}
|
||||
alertDef={alertDef}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
Col,
|
||||
Dropdown as DropDownComponent,
|
||||
Input as InputComponent,
|
||||
} from 'antd';
|
||||
import { Col, Input as InputComponent } from 'antd';
|
||||
import { Typography as TypographyComponent } from '@signozhq/ui/typography';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@@ -34,16 +30,6 @@ export const ButtonContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export const Dropdown = styled(DropDownComponent)`
|
||||
&&& {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
max-width: 150px;
|
||||
min-width: 150px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TextContainer = styled.div`
|
||||
&&& {
|
||||
min-width: 100px;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { AppProvider } from 'providers/App/App';
|
||||
@@ -176,6 +177,7 @@ jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
|
||||
describe('WidgetGraphComponent', () => {
|
||||
it('should show correct menu items when hovering over more options while loading', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const { getByTestId, findByRole, getByText, container } = render(
|
||||
<MockQueryClientProvider>
|
||||
<ErrorModalProvider>
|
||||
@@ -208,7 +210,7 @@ describe('WidgetGraphComponent', () => {
|
||||
expect(skeleton).toBeInTheDocument();
|
||||
|
||||
const moreOptionsButton = getByTestId('widget-header-options');
|
||||
fireEvent.mouseEnter(moreOptionsButton);
|
||||
await user.click(moreOptionsButton);
|
||||
|
||||
const menu = await findByRole('menu');
|
||||
expect(menu).toBeInTheDocument();
|
||||
|
||||
@@ -54,6 +54,17 @@
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
// currently the width of the dropdown menu is set to 100% of the parent container,
|
||||
// which is not desired. This is a workaround to unset that width and allow the dropdown menu to size based on its content.
|
||||
// This is necessary because the dropdown menu can contain items with varying widths, and setting it to 100% can cause layout issues and make the menu look unbalanced.
|
||||
// we should idealy fix this in the dropdown menu component itself, but for now this is a quick fix to ensure the dropdown menu looks correct in the widget header.
|
||||
|
||||
[data-radix-popper-content-wrapper]
|
||||
[data-slot='dropdown-menu-content'].widget-header-dropdown
|
||||
[data-slot='dropdown-menu-item'] {
|
||||
width: unset !important;
|
||||
}
|
||||
|
||||
.widget-api-actions {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -467,6 +467,7 @@ describe('WidgetHeader', () => {
|
||||
|
||||
describe('Create Alerts Menu Item', () => {
|
||||
it('renders Create Alerts menu item with external link icon when included in headerMenuList', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(
|
||||
<WidgetHeader
|
||||
title={TEST_WIDGET_TITLE}
|
||||
@@ -483,7 +484,7 @@ describe('WidgetHeader', () => {
|
||||
|
||||
const moreOptionsIcon = await screen.findByTestId(WIDGET_HEADER_OPTIONS_ID);
|
||||
expect(moreOptionsIcon).toBeInTheDocument();
|
||||
await userEvent.hover(moreOptionsIcon);
|
||||
await user.click(moreOptionsIcon);
|
||||
|
||||
await screen.findByText(CREATE_ALERTS_TEXT);
|
||||
|
||||
@@ -494,6 +495,7 @@ describe('WidgetHeader', () => {
|
||||
});
|
||||
|
||||
it('Create Alerts menu item is enabled and clickable', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockCreateAlertsHandler = jest.fn();
|
||||
const useCreateAlerts = jest.requireMock(
|
||||
'hooks/queryBuilder/useCreateAlerts',
|
||||
@@ -517,12 +519,12 @@ describe('WidgetHeader', () => {
|
||||
expect(useCreateAlerts).toHaveBeenCalledWith(mockWidget, 'dashboardView');
|
||||
|
||||
const moreOptionsIcon = await screen.findByTestId(WIDGET_HEADER_OPTIONS_ID);
|
||||
await userEvent.hover(moreOptionsIcon);
|
||||
await user.click(moreOptionsIcon);
|
||||
|
||||
const createAlertsMenuItem = await screen.findByText(CREATE_ALERTS_TEXT);
|
||||
|
||||
// Verify the menu item is clickable by actually clicking it
|
||||
await userEvent.click(createAlertsMenuItem);
|
||||
await user.click(createAlertsMenuItem);
|
||||
expect(mockCreateAlertsHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,8 @@ import {
|
||||
X,
|
||||
} from '@signozhq/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Dropdown, Input, MenuProps, Tooltip } from 'antd';
|
||||
import { Button, Input, Tooltip } from 'antd';
|
||||
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||
import ErrorPopover from 'components/ErrorPopover/ErrorPopover';
|
||||
@@ -128,7 +129,7 @@ function WidgetHeader({
|
||||
],
|
||||
);
|
||||
|
||||
const onMenuItemSelectHandler: MenuProps['onClick'] = useCallback(
|
||||
const onMenuItemSelectHandler = useCallback(
|
||||
({ key }: { key: string }): void => {
|
||||
if (isTWidgetOptions(key)) {
|
||||
const functionToCall = keyMethodMapping[key];
|
||||
@@ -188,18 +189,8 @@ function WidgetHeader({
|
||||
{
|
||||
key: MenuItemKeys.CreateAlerts,
|
||||
icon: <Bell size="md" />,
|
||||
label: (
|
||||
<span
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'baseline',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{MENUITEM_KEYS_VS_LABELS[MenuItemKeys.CreateAlerts]}
|
||||
<SquareArrowOutUpRight size={10} />
|
||||
</span>
|
||||
),
|
||||
label: MENUITEM_KEYS_VS_LABELS[MenuItemKeys.CreateAlerts],
|
||||
rightIcon: <SquareArrowOutUpRight size="lg" />,
|
||||
isVisible: headerMenuList?.includes(MenuItemKeys.CreateAlerts) || false,
|
||||
disabled: false,
|
||||
},
|
||||
@@ -221,8 +212,10 @@ function WidgetHeader({
|
||||
|
||||
const menu = useMemo(
|
||||
() => ({
|
||||
items: updatedMenuList,
|
||||
onClick: onMenuItemSelectHandler,
|
||||
items: updatedMenuList.map((item) => ({
|
||||
...item,
|
||||
onClick: onMenuItemSelectHandler,
|
||||
})),
|
||||
}),
|
||||
[updatedMenuList, onMenuItemSelectHandler],
|
||||
);
|
||||
@@ -321,7 +314,12 @@ function WidgetHeader({
|
||||
/>
|
||||
)}
|
||||
{menu && Array.isArray(menu.items) && menu.items.length > 0 && (
|
||||
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
|
||||
<DropdownMenuSimple
|
||||
menu={menu}
|
||||
side="bottom"
|
||||
align="end"
|
||||
className="widget-header-dropdown"
|
||||
>
|
||||
<Button
|
||||
data-testid="widget-header-options"
|
||||
className={`widget-header-more-options ${
|
||||
@@ -329,7 +327,7 @@ function WidgetHeader({
|
||||
}`}
|
||||
icon={<EllipsisVertical size="md" />}
|
||||
/>
|
||||
</Dropdown>
|
||||
</DropdownMenuSimple>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface MenuItem {
|
||||
key: MenuItemKeys;
|
||||
icon: ReactNode;
|
||||
label: ReactNode;
|
||||
rightIcon?: ReactNode;
|
||||
isVisible: boolean;
|
||||
disabled: boolean;
|
||||
danger?: boolean;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { MenuItemType } from 'antd/es/menu/hooks/useItems';
|
||||
import type { MenuItem as DropdownMenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
|
||||
import { MenuItemKeys } from './contants';
|
||||
import { MenuItem } from './types';
|
||||
|
||||
export const generateMenuList = (actions: MenuItem[]): MenuItemType[] =>
|
||||
export const generateMenuList = (actions: MenuItem[]): DropdownMenuItem[] =>
|
||||
actions
|
||||
.filter((action: MenuItem) => action.isVisible)
|
||||
.map(({ key, icon: Icon, label, disabled, ...rest }) => ({
|
||||
|
||||
@@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Button, Flex, Input } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Ellipsis, Plus } from '@signozhq/icons';
|
||||
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||
import type { ColumnsType } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
@@ -15,7 +16,6 @@ import type {
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { ErrorType } from 'api/generatedAPIInstance';
|
||||
import { AxiosError } from 'axios';
|
||||
import DropDown from 'components/DropDown/DropDown';
|
||||
import {
|
||||
DynamicColumnsKey,
|
||||
TableDataSource,
|
||||
@@ -323,55 +323,67 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
dataIndex: 'id',
|
||||
key: 'action',
|
||||
width: 10,
|
||||
render: (id: RuletypesRuleDTO['id'], record): JSX.Element => (
|
||||
<div data-testid="alert-actions">
|
||||
<DropDown
|
||||
onDropDownItemClick={(item): void =>
|
||||
alertActionLogEvent(item.key, record)
|
||||
render: (id: RuletypesRuleDTO['id'], record): JSX.Element => {
|
||||
const actionItems = [
|
||||
<ToggleAlertState
|
||||
key="1"
|
||||
disabled={record.disabled ?? false}
|
||||
setData={setData}
|
||||
id={id ?? ''}
|
||||
/>,
|
||||
<ColumnButton
|
||||
key="2"
|
||||
onClick={(e: React.MouseEvent): void =>
|
||||
onEditHandler(record, { newTab: isModifierKeyPressed(e) })
|
||||
}
|
||||
element={[
|
||||
<ToggleAlertState
|
||||
key="1"
|
||||
disabled={record.disabled ?? false}
|
||||
setData={setData}
|
||||
id={id ?? ''}
|
||||
/>,
|
||||
<ColumnButton
|
||||
key="2"
|
||||
onClick={(e: React.MouseEvent): void =>
|
||||
onEditHandler(record, { newTab: isModifierKeyPressed(e) })
|
||||
}
|
||||
type="link"
|
||||
loading={editLoader}
|
||||
>
|
||||
Edit
|
||||
</ColumnButton>,
|
||||
<ColumnButton
|
||||
key="3-new-tab"
|
||||
onClick={(): void => onEditHandler(record, { newTab: true })}
|
||||
type="link"
|
||||
loading={editLoader}
|
||||
>
|
||||
Edit in New Tab
|
||||
</ColumnButton>,
|
||||
<ColumnButton
|
||||
key="3-clone"
|
||||
onClick={onCloneHandler(record)}
|
||||
type="link"
|
||||
loading={cloneLoader}
|
||||
>
|
||||
Clone
|
||||
</ColumnButton>,
|
||||
<DeleteAlert
|
||||
key="4"
|
||||
notifications={notificationsApi}
|
||||
setData={setData}
|
||||
id={id ?? ''}
|
||||
/>,
|
||||
];
|
||||
return (
|
||||
<div data-testid="alert-actions">
|
||||
<DropdownMenuSimple
|
||||
menu={{
|
||||
items: actionItems.map((element, index) => ({
|
||||
key: String(index),
|
||||
label: element,
|
||||
onClick: ({ key }): void => alertActionLogEvent(key, record),
|
||||
})),
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
loading={editLoader}
|
||||
>
|
||||
Edit
|
||||
</ColumnButton>,
|
||||
<ColumnButton
|
||||
key="3"
|
||||
onClick={(): void => onEditHandler(record, { newTab: true })}
|
||||
type="link"
|
||||
loading={editLoader}
|
||||
>
|
||||
Edit in New Tab
|
||||
</ColumnButton>,
|
||||
<ColumnButton
|
||||
key="3"
|
||||
onClick={onCloneHandler(record)}
|
||||
type="link"
|
||||
loading={cloneLoader}
|
||||
>
|
||||
Clone
|
||||
</ColumnButton>,
|
||||
<DeleteAlert
|
||||
key="4"
|
||||
notifications={notificationsApi}
|
||||
setData={setData}
|
||||
id={id ?? ''}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
style={{ color: 'var(--l1-foreground)' }}
|
||||
icon={<Ellipsis size={16} />}
|
||||
/>
|
||||
</DropdownMenuSimple>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Flex,
|
||||
Input,
|
||||
MenuProps,
|
||||
Modal,
|
||||
Popover,
|
||||
Skeleton,
|
||||
@@ -553,7 +552,7 @@ function DashboardsList(): JSX.Element {
|
||||
];
|
||||
|
||||
const getCreateDashboardItems = useMemo(() => {
|
||||
const menuItems: MenuProps['items'] = [
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
label: (
|
||||
<div
|
||||
@@ -711,11 +710,11 @@ function DashboardsList(): JSX.Element {
|
||||
|
||||
{createNewDashboard && (
|
||||
<section className="actions">
|
||||
<Dropdown
|
||||
overlayClassName="new-dashboard-menu"
|
||||
<DropdownMenuSimple
|
||||
className="new-dashboard-menu"
|
||||
menu={{ items: getCreateDashboardItems }}
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
side="bottom"
|
||||
align="end"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
@@ -727,7 +726,7 @@ function DashboardsList(): JSX.Element {
|
||||
>
|
||||
New Dashboard
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</DropdownMenuSimple>
|
||||
<Button
|
||||
type="text"
|
||||
className="learn-more"
|
||||
@@ -756,11 +755,11 @@ function DashboardsList(): JSX.Element {
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
{createNewDashboard && (
|
||||
<Dropdown
|
||||
overlayClassName="new-dashboard-menu"
|
||||
<DropdownMenuSimple
|
||||
className="new-dashboard-menu"
|
||||
menu={{ items: getCreateDashboardItems }}
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
side="bottom"
|
||||
align="end"
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -773,7 +772,7 @@ function DashboardsList(): JSX.Element {
|
||||
>
|
||||
New dashboard
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</DropdownMenuSimple>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,7 +2,13 @@ import { useCallback } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { orange } from '@ant-design/colors';
|
||||
import { Settings } from '@signozhq/icons';
|
||||
import { Dropdown, MenuProps } from 'antd';
|
||||
import {
|
||||
type BaseMenuItem,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@signozhq/ui/dropdown-menu';
|
||||
import {
|
||||
negateOperator,
|
||||
OPERATORS,
|
||||
@@ -135,41 +141,38 @@ function BodyTitleRenderer({
|
||||
viewName,
|
||||
]);
|
||||
|
||||
const onClickHandler: MenuProps['onClick'] = (props): void => {
|
||||
const onClickHandler = (key: string): void => {
|
||||
const mapper = {
|
||||
[DROPDOWN_KEY.FILTER_IN]: filterHandler(true),
|
||||
[DROPDOWN_KEY.FILTER_OUT]: filterHandler(false),
|
||||
[DROPDOWN_KEY.GROUP_BY]: groupByHandler,
|
||||
};
|
||||
|
||||
const handler = mapper[props.key];
|
||||
const handler = mapper[key];
|
||||
|
||||
if (handler) {
|
||||
handler();
|
||||
}
|
||||
};
|
||||
|
||||
const menu: MenuProps = {
|
||||
items: [
|
||||
{
|
||||
key: DROPDOWN_KEY.FILTER_IN,
|
||||
label: `Filter for ${value}`,
|
||||
},
|
||||
{
|
||||
key: DROPDOWN_KEY.FILTER_OUT,
|
||||
label: `Filter out ${value}`,
|
||||
},
|
||||
...(isGroupBySupported
|
||||
? [
|
||||
{
|
||||
key: DROPDOWN_KEY.GROUP_BY,
|
||||
label: `Group by ${nodeKey}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
onClick: onClickHandler,
|
||||
};
|
||||
const menuItems: BaseMenuItem[] = [
|
||||
{
|
||||
key: DROPDOWN_KEY.FILTER_IN,
|
||||
label: `Filter for ${value}`,
|
||||
},
|
||||
{
|
||||
key: DROPDOWN_KEY.FILTER_OUT,
|
||||
label: `Filter out ${value}`,
|
||||
},
|
||||
...(isGroupBySupported
|
||||
? [
|
||||
{
|
||||
key: DROPDOWN_KEY.GROUP_BY,
|
||||
label: `Group by ${nodeKey}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const handleNodeClick = useCallback(
|
||||
(e: React.MouseEvent): void => {
|
||||
@@ -218,15 +221,23 @@ function BodyTitleRenderer({
|
||||
}}
|
||||
onMouseDown={(e): void => e.preventDefault()}
|
||||
>
|
||||
<Dropdown
|
||||
menu={menu}
|
||||
trigger={['click']}
|
||||
dropdownRender={(originNode): React.ReactNode => (
|
||||
<div data-log-detail-ignore="true">{originNode}</div>
|
||||
)}
|
||||
>
|
||||
<Settings style={{ marginRight: 8 }} className="hover-reveal" />
|
||||
</Dropdown>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Settings style={{ marginRight: 8 }} className="hover-reveal" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<div data-log-detail-ignore="true">
|
||||
{menuItems.map((item) => (
|
||||
<DropdownMenuItem
|
||||
key={item.key}
|
||||
onSelect={(): void => onClickHandler(item.key as string)}
|
||||
>
|
||||
{item.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</span>
|
||||
)}
|
||||
{title.toString()}{' '}
|
||||
|
||||
@@ -2,9 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Check, ChevronDown, Plus } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown } from 'antd';
|
||||
import { useListUsers } from 'api/generated/services/users';
|
||||
import EditMemberDrawer from 'components/EditMemberDrawer/EditMemberDrawer';
|
||||
import InviteMembersModal from 'components/InviteMembersModal/InviteMembersModal';
|
||||
@@ -95,7 +94,7 @@ function MembersSettings(): JSX.Element {
|
||||
).length;
|
||||
const totalCount = allMembers.length;
|
||||
|
||||
const filterMenuItems: MenuProps['items'] = [
|
||||
const filterMenuItems: MenuItem[] = [
|
||||
{
|
||||
key: FilterMode.All,
|
||||
label: (
|
||||
@@ -171,10 +170,9 @@ function MembersSettings(): JSX.Element {
|
||||
</div>
|
||||
|
||||
<div className="members-settings__controls">
|
||||
<Dropdown
|
||||
<DropdownMenuSimple
|
||||
menu={{ items: filterMenuItems }}
|
||||
trigger={['click']}
|
||||
overlayClassName="members-filter-dropdown"
|
||||
className="members-filter-dropdown"
|
||||
>
|
||||
<Button
|
||||
variant="solid"
|
||||
@@ -184,7 +182,7 @@ function MembersSettings(): JSX.Element {
|
||||
<span>{filterLabel}</span>
|
||||
<ChevronDown size={12} className="members-filter-trigger__chevron" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</DropdownMenuSimple>
|
||||
|
||||
<div className="members-settings__search">
|
||||
<Input
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TypesUserDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { fireEvent, render, screen } from 'tests/test-utils';
|
||||
|
||||
@@ -76,14 +77,15 @@ describe('MembersSettings (integration)', () => {
|
||||
});
|
||||
|
||||
it('filters to pending invites via the filter dropdown', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<MembersSettings />);
|
||||
|
||||
await screen.findByText('Alice Smith');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /all members/i }));
|
||||
await user.click(screen.getByRole('button', { name: /all members/i }));
|
||||
|
||||
const pendingOption = await screen.findByText(/pending invites/i);
|
||||
fireEvent.click(pendingOption);
|
||||
await user.click(pendingOption);
|
||||
|
||||
await screen.findByText('charlie@signoz.io');
|
||||
expect(screen.queryByText('Alice Smith')).not.toBeInTheDocument();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Dropdown, Skeleton } from 'antd';
|
||||
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||
import { Skeleton } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
useGetMetricAlerts,
|
||||
@@ -126,12 +127,11 @@ function DashboardsAndAlertsPopover({
|
||||
return (
|
||||
<div className="dashboards-and-alerts-popover-container">
|
||||
{dashboardsPopoverContent && (
|
||||
<Dropdown
|
||||
<DropdownMenuSimple
|
||||
menu={{
|
||||
items: dashboardsPopoverContent,
|
||||
}}
|
||||
placement="bottomLeft"
|
||||
trigger={['click']}
|
||||
align="start"
|
||||
>
|
||||
<div
|
||||
className="dashboards-and-alerts-popover dashboards-popover"
|
||||
@@ -142,15 +142,14 @@ function DashboardsAndAlertsPopover({
|
||||
{pluralize(dashboards.length, 'dashboard')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</DropdownMenuSimple>
|
||||
)}
|
||||
{alertsPopoverContent && (
|
||||
<Dropdown
|
||||
<DropdownMenuSimple
|
||||
menu={{
|
||||
items: alertsPopoverContent,
|
||||
}}
|
||||
placement="bottomLeft"
|
||||
trigger={['click']}
|
||||
align="start"
|
||||
>
|
||||
<div
|
||||
className="dashboards-and-alerts-popover alerts-popover"
|
||||
@@ -161,7 +160,7 @@ function DashboardsAndAlertsPopover({
|
||||
{pluralize(alerts.length, 'alert rule')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</DropdownMenuSimple>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -104,38 +104,33 @@
|
||||
height: 200px;
|
||||
background-color: var(--l1-border);
|
||||
overflow: hidden !important;
|
||||
.ant-dropdown-menu {
|
||||
padding: 0;
|
||||
|
||||
.ant-dropdown-menu-item {
|
||||
padding: 4px;
|
||||
.ant-checkbox-wrapper {
|
||||
padding: 2px 8px !important;
|
||||
}
|
||||
padding: 4px;
|
||||
.ant-checkbox-wrapper {
|
||||
padding: 2px 8px !important;
|
||||
}
|
||||
|
||||
.attribute-columns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 160px;
|
||||
overflow: scroll;
|
||||
}
|
||||
.attribute-columns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 160px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.attribute-columns::-webkit-scrollbar {
|
||||
width: 3px; /* Width of the scrollbar */
|
||||
}
|
||||
.attribute-columns::-webkit-scrollbar {
|
||||
width: 3px; /* Width of the scrollbar */
|
||||
}
|
||||
|
||||
.attribute-columns::-webkit-scrollbar-track {
|
||||
background: var(--l1-border); /* Color of the track */
|
||||
}
|
||||
.attribute-columns::-webkit-scrollbar-track {
|
||||
background: var(--l1-border); /* Color of the track */
|
||||
}
|
||||
|
||||
.attribute-columns::-webkit-scrollbar-thumb {
|
||||
background: var(--l2-foreground); /* Color of the thumb */
|
||||
border-radius: 4px; /* Roundness of the thumb */
|
||||
}
|
||||
.attribute-columns::-webkit-scrollbar-thumb {
|
||||
background: var(--l2-foreground); /* Color of the thumb */
|
||||
border-radius: 4px; /* Roundness of the thumb */
|
||||
}
|
||||
|
||||
.attribute-columns::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--l1-border); /* Color of the thumb on hover */
|
||||
}
|
||||
}
|
||||
.attribute-columns::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--l1-border); /* Color of the thumb on hover */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,12 @@ import {
|
||||
DropResult,
|
||||
} from 'react-beautiful-dnd';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Dropdown, Input, MenuProps, Tooltip } from 'antd';
|
||||
import { Button, Divider, Input, Tooltip } from 'antd';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@signozhq/ui/dropdown-menu';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { FieldDataType } from 'api/v5/v5';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
@@ -159,34 +164,12 @@ function ExplorerColumnsRenderer({
|
||||
debouncedSetQuerySearchText(e.target.value);
|
||||
};
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: 'search',
|
||||
label: (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
className="explorer-columns-search"
|
||||
value={searchText}
|
||||
onChange={handleSearchChange}
|
||||
prefix={<Search size={16} style={{ padding: '6px' }} />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'columns',
|
||||
label: (
|
||||
<ExplorerAttributeColumns
|
||||
isLoading={isLoading}
|
||||
data={data}
|
||||
searchText={searchText}
|
||||
isAttributeKeySelected={isAttributeKeySelected}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
dataSource={initialDataSource}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
const handleOpenChange = (nextOpen: boolean): void => {
|
||||
setOpen(nextOpen);
|
||||
if (nextOpen) {
|
||||
setSearchText('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeSelectedLogField = (name: string): void => {
|
||||
if (
|
||||
@@ -238,13 +221,6 @@ function ExplorerColumnsRenderer({
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDropdown = (): void => {
|
||||
setOpen(!open);
|
||||
if (!open) {
|
||||
setSearchText('');
|
||||
}
|
||||
};
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
return (
|
||||
@@ -327,25 +303,38 @@ function ExplorerColumnsRenderer({
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<div>
|
||||
<Dropdown
|
||||
menu={{ items }}
|
||||
arrow
|
||||
placement="top"
|
||||
open={open}
|
||||
overlayClassName="explorer-columns-dropdown"
|
||||
>
|
||||
<Button
|
||||
className="action-btn"
|
||||
data-testid="add-columns-button"
|
||||
icon={
|
||||
<CirclePlus
|
||||
size={16}
|
||||
color={isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100}
|
||||
/>
|
||||
}
|
||||
onClick={toggleDropdown}
|
||||
/>
|
||||
</Dropdown>
|
||||
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="action-btn"
|
||||
data-testid="add-columns-button"
|
||||
icon={
|
||||
<CirclePlus
|
||||
size={16}
|
||||
color={isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" className="explorer-columns-dropdown">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
className="explorer-columns-search"
|
||||
value={searchText}
|
||||
onChange={handleSearchChange}
|
||||
prefix={<Search size={16} style={{ padding: '6px' }} />}
|
||||
/>
|
||||
<ExplorerAttributeColumns
|
||||
isLoading={isLoading}
|
||||
data={data}
|
||||
searchText={searchText}
|
||||
isAttributeKeySelected={isAttributeKeySelected}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
dataSource={initialDataSource}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -146,6 +146,7 @@ describe('ExplorerColumnsRenderer', () => {
|
||||
});
|
||||
|
||||
it('opens and closes the dropdown', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(
|
||||
<Wrapper>
|
||||
<ExplorerColumnsRenderer
|
||||
@@ -158,12 +159,12 @@ describe('ExplorerColumnsRenderer', () => {
|
||||
);
|
||||
|
||||
const addButton = screen.getByTestId('add-columns-button');
|
||||
await userEvent.click(addButton);
|
||||
await user.click(addButton);
|
||||
|
||||
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument();
|
||||
expect(screen.getByText('attribute1')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(addButton);
|
||||
await user.click(addButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Plus, Trash2 } from '@signozhq/icons';
|
||||
import { ContextLinkProps, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
|
||||
import VariablesDropdown from './VariablesDropdown';
|
||||
import VariablesPopover from './VariablesPopover';
|
||||
|
||||
import './UpdateContextLinks.styles.scss';
|
||||
|
||||
@@ -71,7 +71,7 @@ function UpdateContextLinks({
|
||||
customVariables: fieldVariables,
|
||||
});
|
||||
|
||||
// Transform variables into the format expected by VariablesDropdown
|
||||
// Transform variables into the format expected by VariablesPopover
|
||||
const transformedVariables = useMemo(
|
||||
() => transformContextVariables(variables),
|
||||
[variables],
|
||||
@@ -224,7 +224,9 @@ function UpdateContextLinks({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<VariablesDropdown
|
||||
{/* TODO: replace with AutoComplete with options for variables and
|
||||
previously used URLs for better UX */}
|
||||
<VariablesPopover
|
||||
onVariableSelect={handleVariableSelect}
|
||||
variables={transformedVariables}
|
||||
>
|
||||
@@ -252,7 +254,7 @@ function UpdateContextLinks({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</VariablesDropdown>
|
||||
</VariablesPopover>
|
||||
</Form.Item>
|
||||
|
||||
{/* Remove the separate variables section */}
|
||||
@@ -282,7 +284,7 @@ function UpdateContextLinks({
|
||||
/>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<VariablesDropdown
|
||||
<VariablesPopover
|
||||
onVariableSelect={(variableName, cursorPosition): void =>
|
||||
handleParamVariableSelect(index, variableName, cursorPosition)
|
||||
}
|
||||
@@ -311,7 +313,7 @@ function UpdateContextLinks({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</VariablesDropdown>
|
||||
</VariablesPopover>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<Button
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
.variables-dropdown-container {
|
||||
.url-input-trigger {
|
||||
width: 100%;
|
||||
|
||||
.url-input-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Override Ant Design dropdown styles
|
||||
.ant-dropdown-menu {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.variable-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.variable-source {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Dropdown } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import './VariablesDropdown.styles.scss';
|
||||
|
||||
interface VariablesDropdownProps {
|
||||
onVariableSelect: (variableName: string, cursorPosition?: number) => void;
|
||||
variables: VariableItem[];
|
||||
children: (props: {
|
||||
onVariableSelect: (variableName: string, cursorPosition?: number) => void;
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
cursorPosition: number | null;
|
||||
setCursorPosition: (position: number | null) => void;
|
||||
}) => ReactNode;
|
||||
}
|
||||
|
||||
interface VariableItem {
|
||||
name: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
function VariablesDropdown({
|
||||
onVariableSelect,
|
||||
variables,
|
||||
children,
|
||||
}: VariablesDropdownProps): JSX.Element {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [cursorPosition, setCursorPosition] = useState<number | null>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Click outside handler
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent): void {
|
||||
if (
|
||||
wrapperRef.current &&
|
||||
!wrapperRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
return (): void => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const dropdownItems = useMemo(
|
||||
() =>
|
||||
variables.map((v) => ({
|
||||
key: v.name,
|
||||
label: (
|
||||
<div className="variable-row">
|
||||
<Typography.Text className="variable-name">{`{{${v.name}}}`}</Typography.Text>
|
||||
<Typography.Text className="variable-source">{v.source}</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
})),
|
||||
[variables],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="variables-dropdown-container" ref={wrapperRef}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: dropdownItems,
|
||||
onClick: ({ key }): void => {
|
||||
const variableName = key as string;
|
||||
onVariableSelect(`{{${variableName}}}`, cursorPosition || undefined);
|
||||
setIsOpen(false);
|
||||
},
|
||||
}}
|
||||
open={isOpen}
|
||||
placement="bottomLeft"
|
||||
trigger={['click']}
|
||||
getPopupContainer={(): HTMLElement => wrapperRef.current || document.body}
|
||||
>
|
||||
{children({
|
||||
onVariableSelect,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
cursorPosition,
|
||||
setCursorPosition,
|
||||
})}
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default VariablesDropdown;
|
||||
@@ -0,0 +1,74 @@
|
||||
.variables-popover-container {
|
||||
.url-input-trigger {
|
||||
width: 100%;
|
||||
|
||||
.url-input-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.variables-popover-anchor-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.variables-popover-content {
|
||||
// antd Modal uses z-index ~1000; popover must sit above it.
|
||||
z-index: 1100;
|
||||
padding: 4px 0;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-width: var(--radix-popover-trigger-width);
|
||||
}
|
||||
|
||||
.variables-popover-empty {
|
||||
padding: 8px 12px;
|
||||
color: var(--l3-foreground, #999);
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.variables-popover-item {
|
||||
all: unset;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
color: var(--l1-foreground);
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--l1-background-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.variable-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
|
||||
.variable-name,
|
||||
.variable-source {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.variable-name {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.variable-source {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Uses Popover (not DropdownMenu like the rest of the antd-dropdown migration):
|
||||
// DropdownMenuTrigger preventDefaults pointerdown, breaking input focus and
|
||||
// dismissing on every keystroke. PopoverAnchor is a passive positioning element.
|
||||
import { ReactNode, useRef, useState } from 'react';
|
||||
import { Popover, PopoverAnchor, PopoverContent } from '@signozhq/ui/popover';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import './VariablesPopover.styles.scss';
|
||||
|
||||
interface VariablesPopoverProps {
|
||||
onVariableSelect: (variableName: string, cursorPosition?: number) => void;
|
||||
variables: VariableItem[];
|
||||
children: (props: {
|
||||
onVariableSelect: (variableName: string, cursorPosition?: number) => void;
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
cursorPosition: number | null;
|
||||
setCursorPosition: (position: number | null) => void;
|
||||
}) => ReactNode;
|
||||
}
|
||||
|
||||
interface VariableItem {
|
||||
name: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
function VariablesPopover({
|
||||
onVariableSelect,
|
||||
variables,
|
||||
children,
|
||||
}: VariablesPopoverProps): JSX.Element {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [cursorPosition, setCursorPosition] = useState<number | null>(null);
|
||||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleOpenChange = (open: boolean): void => {
|
||||
// Accept "close" events from the popover (outside-click, Esc) but ignore
|
||||
// opens — opening is driven by the input's onFocus in the consumer.
|
||||
if (!open) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="variables-popover-container">
|
||||
<Popover open={isOpen} onOpenChange={handleOpenChange} modal={false}>
|
||||
<PopoverAnchor asChild>
|
||||
<div className="variables-popover-anchor-wrap" ref={anchorRef}>
|
||||
{children({
|
||||
onVariableSelect,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
cursorPosition,
|
||||
setCursorPosition,
|
||||
})}
|
||||
</div>
|
||||
</PopoverAnchor>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
className="variables-popover-content"
|
||||
onOpenAutoFocus={(e): void => e.preventDefault()}
|
||||
onCloseAutoFocus={(e): void => e.preventDefault()}
|
||||
onInteractOutside={(e): void => {
|
||||
// Keep the popover open while interacting with the anchor (the input),
|
||||
// otherwise typing/clicking the input would close it immediately.
|
||||
const target = e.target as Node | null;
|
||||
if (target && anchorRef.current?.contains(target)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
onFocusOutside={(e): void => {
|
||||
const target = e.target as Node | null;
|
||||
if (target && anchorRef.current?.contains(target)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{variables.length === 0 ? (
|
||||
<div className="variables-popover-empty">No variables available</div>
|
||||
) : (
|
||||
variables.map((v) => (
|
||||
<button
|
||||
key={v.name}
|
||||
type="button"
|
||||
className="variables-popover-item"
|
||||
onMouseDown={(e): void => {
|
||||
// Prevent the input from losing focus when clicking an item.
|
||||
e.preventDefault();
|
||||
}}
|
||||
onClick={(): void => {
|
||||
onVariableSelect(`{{${v.name}}}`, cursorPosition || undefined);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="variable-row">
|
||||
<Typography.Text className="variable-name">{`{{${v.name}}}`}</Typography.Text>
|
||||
<Typography.Text className="variable-source">
|
||||
{v.source}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default VariablesPopover;
|
||||
@@ -204,7 +204,7 @@ const processContextLinks = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Transforms context variables into the format expected by VariablesDropdown
|
||||
* Transforms context variables into the format expected by VariablesPopover
|
||||
* @param variables - Array of context variables from useContextVariables
|
||||
* @returns Array of transformed variables with proper source descriptions
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { ChevronDown } from '@signozhq/icons';
|
||||
import { Button, ColorPicker, Dropdown, MenuProps, Space } from 'antd';
|
||||
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
import { Button, ColorPicker, Space } from 'antd';
|
||||
import type { Color } from 'antd/es/color-picker';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
|
||||
@@ -26,7 +27,7 @@ function ColorSelector({
|
||||
setColorFromPicker(hex);
|
||||
};
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
const items: MenuItem[] = [
|
||||
{
|
||||
key: 'Red',
|
||||
label: <CustomColor color="Red" />,
|
||||
@@ -62,7 +63,7 @@ function ColorSelector({
|
||||
];
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items }} trigger={['click']}>
|
||||
<DropdownMenuSimple menu={{ items }}>
|
||||
<Button
|
||||
onClick={(e): void => e.preventDefault()}
|
||||
className="color-selector-button"
|
||||
@@ -72,7 +73,7 @@ function ColorSelector({
|
||||
<ChevronDown size="md" />
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</DropdownMenuSimple>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -295,37 +295,6 @@
|
||||
|
||||
.slider-container {
|
||||
width: calc(100% - 16px);
|
||||
|
||||
.ant-slider .ant-slider-mark {
|
||||
margin-top: 12px;
|
||||
|
||||
.ant-slider-mark-text {
|
||||
color: var(--l3-foreground);
|
||||
font-variant-numeric: lining-nums tabular-nums stacked-fractions
|
||||
slashed-zero;
|
||||
font-feature-settings:
|
||||
'dlig' on,
|
||||
'salt' on,
|
||||
'cpsp' on,
|
||||
'case' on;
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.logs-slider-container {
|
||||
.ant-slider .ant-slider-mark {
|
||||
.ant-slider-mark-text {
|
||||
&:last-child {
|
||||
left: calc(100% - 8px) !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.do-later-container {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Slider } from 'antd';
|
||||
import { Slider } from '@signozhq/ui/slider';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ArrowRight, LoaderCircle, Minus } from '@signozhq/icons';
|
||||
@@ -204,23 +204,23 @@ function OptimiseSignozNeeds({
|
||||
<label className="question-slider" htmlFor="organisationName">
|
||||
Logs / Day
|
||||
</label>
|
||||
<div className="slider-container logs-slider-container">
|
||||
<div className="slider-container">
|
||||
<div>
|
||||
<Slider
|
||||
min={0}
|
||||
max={100}
|
||||
value={sliderValues.logsPerDay}
|
||||
marks={marks}
|
||||
onChange={(value: number): void =>
|
||||
handleSliderChange('logsPerDay', value)
|
||||
onChange={(value): void =>
|
||||
handleSliderChange('logsPerDay', value as number)
|
||||
}
|
||||
styles={{
|
||||
track: {
|
||||
background: '#4E74F8',
|
||||
range: {
|
||||
backgroundColor: '#4E74F8',
|
||||
},
|
||||
}}
|
||||
tooltip={{
|
||||
formatter: (): string => `${logsPerDayValue.toLocaleString()} GB`, // Show whole number
|
||||
formatter: (): string => `${logsPerDayValue.toLocaleString()} GB`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -238,16 +238,16 @@ function OptimiseSignozNeeds({
|
||||
max={100}
|
||||
value={sliderValues.hostsPerDay}
|
||||
marks={hostMarks}
|
||||
onChange={(value: number): void =>
|
||||
handleSliderChange('hostsPerDay', value)
|
||||
onChange={(value): void =>
|
||||
handleSliderChange('hostsPerDay', value as number)
|
||||
}
|
||||
styles={{
|
||||
track: {
|
||||
background: '#4E74F8',
|
||||
range: {
|
||||
backgroundColor: '#4E74F8',
|
||||
},
|
||||
}}
|
||||
tooltip={{
|
||||
formatter: (): string => `${hostsPerDayValue.toLocaleString()}`, // Show whole number
|
||||
formatter: (): string => `${hostsPerDayValue.toLocaleString()}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -265,16 +265,16 @@ function OptimiseSignozNeeds({
|
||||
max={100}
|
||||
value={sliderValues.services}
|
||||
marks={serviceMarks}
|
||||
onChange={(value: number): void =>
|
||||
handleSliderChange('services', value)
|
||||
onChange={(value): void =>
|
||||
handleSliderChange('services', value as number)
|
||||
}
|
||||
styles={{
|
||||
track: {
|
||||
background: '#4E74F8',
|
||||
range: {
|
||||
backgroundColor: '#4E74F8',
|
||||
},
|
||||
}}
|
||||
tooltip={{
|
||||
formatter: (): string => `${servicesValue.toLocaleString()}`, // Show whole number
|
||||
formatter: (): string => `${servicesValue.toLocaleString()}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Check, ChevronDown, Plus } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown } from 'antd';
|
||||
import { useListServiceAccounts } from 'api/generated/services/serviceaccount';
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
import CreateServiceAccountModal from 'components/CreateServiceAccountModal/CreateServiceAccountModal';
|
||||
@@ -134,7 +133,7 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
|
||||
const totalCount = allAccounts.length;
|
||||
|
||||
const filterMenuItems: MenuProps['items'] = [
|
||||
const filterMenuItems: MenuItem[] = [
|
||||
{
|
||||
key: FilterMode.All,
|
||||
label: (
|
||||
@@ -231,10 +230,9 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
) : (
|
||||
<div className="sa-settings__list-section">
|
||||
<div className="sa-settings__controls">
|
||||
<Dropdown
|
||||
<DropdownMenuSimple
|
||||
menu={{ items: filterMenuItems }}
|
||||
trigger={['click']}
|
||||
overlayClassName="sa-settings-filter-dropdown"
|
||||
className="sa-settings-filter-dropdown"
|
||||
>
|
||||
<Button
|
||||
variant="solid"
|
||||
@@ -247,7 +245,7 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
className="sa-settings-filter-trigger__chevron"
|
||||
/>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</DropdownMenuSimple>
|
||||
|
||||
<div className="sa-settings__search">
|
||||
<Input
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
@@ -129,6 +130,7 @@ describe('ServiceAccountsSettings (integration)', () => {
|
||||
});
|
||||
|
||||
it('filter dropdown to "Active" hides DISABLED accounts', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(
|
||||
<NuqsTestingAdapter>
|
||||
<ServiceAccountsSettings />
|
||||
@@ -137,10 +139,10 @@ describe('ServiceAccountsSettings (integration)', () => {
|
||||
|
||||
await screen.findByText('CI Bot');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /All accounts/i }));
|
||||
await user.click(screen.getByRole('button', { name: /All accounts/i }));
|
||||
|
||||
const activeOption = await screen.findByText(/Active ⎯/i);
|
||||
fireEvent.click(activeOption);
|
||||
await user.click(activeOption);
|
||||
|
||||
await screen.findByText('CI Bot');
|
||||
expect(screen.queryByText('Legacy Bot')).not.toBeInTheDocument();
|
||||
|
||||
@@ -662,7 +662,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.pinned):hover,
|
||||
&:not(.pinned).is-hovered,
|
||||
&.dropdown-open {
|
||||
flex: 0 0 240px;
|
||||
max-width: 240px;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
MouseEvent,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -25,7 +26,14 @@ import {
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Button, Dropdown, MenuProps, Modal, Tooltip } from 'antd';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@signozhq/ui/dropdown-menu';
|
||||
import { Button, MenuProps, Modal, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { Logout } from 'api/utils';
|
||||
import updateUserPreference from 'api/v1/user/preferences/name/update';
|
||||
@@ -162,7 +170,9 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
|
||||
const [hasScroll, setHasScroll] = useState(false);
|
||||
const navTopSectionRef = useRef<HTMLDivElement>(null);
|
||||
const sidenavRef = useRef<HTMLDivElement>(null);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const isDropdownOpenRef = useRef(false);
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [pinnedMenuItems, setPinnedMenuItems] = useState<SidebarItem[]>([]);
|
||||
@@ -175,9 +185,27 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
}, []);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
// When the dropdown is open its content renders in a portal outside
|
||||
// the sidenav, which causes the browser to fire mouseleave on the
|
||||
// sidenav. Keep the sidenav expanded in that case.
|
||||
if (isDropdownOpenRef.current) {
|
||||
return;
|
||||
}
|
||||
setIsHovered(false);
|
||||
}, []);
|
||||
|
||||
const handleDropdownOpenChange = useCallback((open: boolean): void => {
|
||||
isDropdownOpenRef.current = open;
|
||||
setIsDropdownOpen(open);
|
||||
if (!open) {
|
||||
// Re-sync hover state on close: the cursor may have moved to the
|
||||
// portal content (outside .sideNav), so mouseleave never fired.
|
||||
requestAnimationFrame(() => {
|
||||
setIsHovered(sidenavRef.current?.matches(':hover') ?? false);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkScroll = useCallback((): void => {
|
||||
if (navTopSectionRef.current) {
|
||||
const { scrollHeight, clientHeight, scrollTop } = navTopSectionRef.current;
|
||||
@@ -959,9 +987,11 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
return (
|
||||
<div className={cx('sidenav-container', isPinned && 'pinned')}>
|
||||
<div
|
||||
ref={sidenavRef}
|
||||
className={cx(
|
||||
'sideNav',
|
||||
isPinned && 'pinned',
|
||||
isHovered && 'is-hovered',
|
||||
isDropdownOpen && 'dropdown-open',
|
||||
)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
@@ -1182,46 +1212,95 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
{isAIAssistantEnabled && renderNavItems([aiAssistantMenuItem], false)}
|
||||
|
||||
<div className="nav-dropdown-item">
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: helpSupportDropdownMenuItems,
|
||||
onClick: handleHelpSupportMenuItemClick,
|
||||
}}
|
||||
placement="topLeft"
|
||||
overlayClassName="nav-dropdown-overlay help-support-dropdown"
|
||||
trigger={['click']}
|
||||
onOpenChange={(open): void => setIsDropdownOpen(open)}
|
||||
>
|
||||
<div className="nav-item">
|
||||
<div className="nav-item-data" data-testid="help-support-nav-item">
|
||||
<div className="nav-item-icon">{helpSupportMenuItem.icon}</div>
|
||||
<DropdownMenu onOpenChange={handleDropdownOpenChange}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="nav-item">
|
||||
<div className="nav-item-data" data-testid="help-support-nav-item">
|
||||
<div className="nav-item-icon">{helpSupportMenuItem.icon}</div>
|
||||
|
||||
<div className="nav-item-label">{helpSupportMenuItem.label}</div>
|
||||
<div className="nav-item-label">{helpSupportMenuItem.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="nav-dropdown-overlay help-support-dropdown"
|
||||
>
|
||||
{helpSupportDropdownMenuItems.map((item, idx) => {
|
||||
if ('type' in item) {
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
return <DropdownMenuSeparator key={`help-sep-${idx}`} />;
|
||||
}
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={String(item.key)}
|
||||
leftIcon={item.icon}
|
||||
onClick={(e): void =>
|
||||
handleHelpSupportMenuItemClick({
|
||||
...item,
|
||||
key: String(item.key),
|
||||
domEvent: e.nativeEvent,
|
||||
} as unknown as SidebarItem)
|
||||
}
|
||||
>
|
||||
{item.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div className="nav-dropdown-item">
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: userSettingsDropdownMenuItems,
|
||||
onClick: handleSettingsMenuItemClick,
|
||||
}}
|
||||
placement="topLeft"
|
||||
overlayClassName="nav-dropdown-overlay settings-dropdown"
|
||||
trigger={['click']}
|
||||
onOpenChange={(open): void => setIsDropdownOpen(open)}
|
||||
>
|
||||
<div className={cx('nav-item', isSettingsPage && 'active')}>
|
||||
<div className="nav-item-active-marker" />
|
||||
<div className="nav-item-data" data-testid="settings-nav-item">
|
||||
<div className="nav-item-icon">{userSettingsMenuItem.icon}</div>
|
||||
<DropdownMenu onOpenChange={handleDropdownOpenChange}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className={cx('nav-item', isSettingsPage && 'active')}>
|
||||
<div className="nav-item-active-marker" />
|
||||
<div className="nav-item-data" data-testid="settings-nav-item">
|
||||
<div className="nav-item-icon">{userSettingsMenuItem.icon}</div>
|
||||
|
||||
<div className="nav-item-label">{userSettingsMenuItem.label}</div>
|
||||
<div className="nav-item-label">{userSettingsMenuItem.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="nav-dropdown-overlay settings-dropdown"
|
||||
>
|
||||
{(userSettingsDropdownMenuItems ?? []).map((item, idx) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
if ('type' in item && item.type === 'divider') {
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
return <DropdownMenuSeparator key={`settings-sep-${idx}`} />;
|
||||
}
|
||||
const settingsItem = item as {
|
||||
key?: string | number;
|
||||
label?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
disabled?: boolean;
|
||||
};
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={String(settingsItem.key)}
|
||||
leftIcon={settingsItem.icon}
|
||||
disabled={settingsItem.disabled}
|
||||
onClick={(e): void =>
|
||||
handleSettingsMenuItemClick({
|
||||
key: String(settingsItem.key),
|
||||
domEvent: e.nativeEvent,
|
||||
} as unknown as SidebarItem)
|
||||
}
|
||||
>
|
||||
{settingsItem.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
} from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Slider } from 'antd';
|
||||
import type { SliderRangeProps } from 'antd/lib/slider';
|
||||
import { Slider } from '@signozhq/ui/slider';
|
||||
import getFilters from 'api/trace/getFilters';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -169,16 +168,15 @@ function Duration(): JSX.Element {
|
||||
debouncedFunction(min, max);
|
||||
};
|
||||
|
||||
const onRangeHandler: SliderRangeProps['onChange'] = ([min, max]) => {
|
||||
const onRangeHandler = (value: number | number[]): void => {
|
||||
const [min, max] = value as number[];
|
||||
updatedUrl(min, max);
|
||||
};
|
||||
|
||||
const TipComponent = useCallback((value: undefined | number) => {
|
||||
if (value === undefined) {
|
||||
return <div />;
|
||||
}
|
||||
return <div>{`${value?.toString()}ms`}</div>;
|
||||
}, []);
|
||||
const TipComponent = useCallback(
|
||||
(value: number) => <div>{`${value.toString()}ms`}</div>,
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -210,7 +208,8 @@ function Duration(): JSX.Element {
|
||||
max={Number(getMs(String(preLocalMaxDuration.current || 0)))}
|
||||
range
|
||||
tooltip={{ formatter: TipComponent }}
|
||||
onChange={([min, max]): void => {
|
||||
onChange={(value): void => {
|
||||
const [min, max] = value as number[];
|
||||
onRangeSliderHandler([String(min), String(max)]);
|
||||
}}
|
||||
onAfterChange={onRangeHandler}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
fillMissingXAxisTimestamps,
|
||||
getXAxisTimestamps,
|
||||
} from 'container/DashboardContainer/visualization/panels/utils';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
/**
|
||||
* Checks if a value is invalid for plotting
|
||||
*
|
||||
@@ -52,6 +58,28 @@ export function normalizePlotValue(
|
||||
return value as number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if at most one entry in `values` is a valid plot value.
|
||||
*
|
||||
* Used to decide whether a series should render as a single point (drawStyle:
|
||||
* Points) vs a line — a continuous line with only one visible sample is
|
||||
* invisible to the user.
|
||||
*/
|
||||
export function hasSingleVisiblePoint(
|
||||
values: ReadonlyArray<readonly [unknown, unknown]> | undefined,
|
||||
): boolean {
|
||||
let validPointCount = 0;
|
||||
for (const [, rawValue] of values ?? []) {
|
||||
if (!isInvalidPlotValue(rawValue)) {
|
||||
validPointCount += 1;
|
||||
if (validPointCount > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export interface SeriesSpanGapsOption {
|
||||
spanGaps?: boolean | number;
|
||||
}
|
||||
@@ -226,3 +254,21 @@ export function applySpanGapsToAlignedData(
|
||||
|
||||
return [newX, ...transformedSeries] as uPlot.AlignedData;
|
||||
}
|
||||
|
||||
/** * Transforms raw API response into aligned data format expected by uPlot.
|
||||
*
|
||||
* The API response contains multiple series of time-value pairs, each with its
|
||||
* own set of timestamps. uPlot requires a single shared x-axis (timestamps)
|
||||
* and separate y-value arrays for each series, aligned by index. This function
|
||||
* extracts the unique sorted timestamps across all series and fills in missing
|
||||
* values with null to maintain alignment.
|
||||
*/
|
||||
export const prepareChartData = (
|
||||
apiResponse: MetricRangePayloadProps,
|
||||
): uPlot.AlignedData => {
|
||||
const seriesList = apiResponse?.data?.result || [];
|
||||
const timestampArr = getXAxisTimestamps(seriesList);
|
||||
const yAxisValuesArr = fillMissingXAxisTimestamps(timestampArr, seriesList);
|
||||
|
||||
return [timestampArr, ...yAxisValuesArr];
|
||||
};
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
border-color: var(--l1-border);
|
||||
margin: 0;
|
||||
}
|
||||
.dropdown-icon {
|
||||
margin-right: 4px;
|
||||
.dropdown-trigger-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.dropdown-menu {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Divider, Dropdown, MenuProps, Tooltip } from 'antd';
|
||||
import { Button, Divider, Tooltip } from 'antd';
|
||||
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { Copy, Ellipsis, PenLine, Trash2 } from '@signozhq/icons';
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
} from 'pages/AlertDetails/hooks';
|
||||
import CopyToClipboard from 'periscope/components/CopyToClipboard';
|
||||
import { useAlertRule } from 'providers/Alert';
|
||||
import { CSSProperties } from 'styled-components';
|
||||
import { NEW_ALERT_SCHEMA_VERSION } from 'types/api/alerts/alertTypesV2';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
@@ -21,16 +21,6 @@ import RenameModal from './RenameModal';
|
||||
|
||||
import './ActionButtons.styles.scss';
|
||||
|
||||
const menuItemStyle: CSSProperties = {
|
||||
fontSize: '14px',
|
||||
letterSpacing: '0.14px',
|
||||
};
|
||||
|
||||
const menuItemStyleV2: CSSProperties = {
|
||||
fontSize: '13px',
|
||||
letterSpacing: '0.13px',
|
||||
};
|
||||
|
||||
function AlertActionButtons({
|
||||
ruleId,
|
||||
alertDetails,
|
||||
@@ -68,9 +58,7 @@ function AlertActionButtons({
|
||||
|
||||
const isV2Alert = alertDetails.schemaVersion === NEW_ALERT_SCHEMA_VERSION;
|
||||
|
||||
const finalMenuItemStyle = isV2Alert ? menuItemStyleV2 : menuItemStyle;
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
const menuItems: MenuItem[] = [
|
||||
...(!isV2Alert
|
||||
? [
|
||||
{
|
||||
@@ -78,7 +66,6 @@ function AlertActionButtons({
|
||||
label: 'Rename',
|
||||
icon: <PenLine size={16} color={Color.BG_VANILLA_400} />,
|
||||
onClick: handleRename,
|
||||
style: finalMenuItemStyle,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -87,17 +74,13 @@ function AlertActionButtons({
|
||||
label: 'Duplicate',
|
||||
icon: <Copy size={16} color={Color.BG_VANILLA_400} />,
|
||||
onClick: handleAlertDuplicate,
|
||||
style: finalMenuItemStyle,
|
||||
},
|
||||
{
|
||||
key: 'delete-rule',
|
||||
label: 'Delete',
|
||||
icon: <Trash2 size={16} color={Color.BG_CHERRY_400} />,
|
||||
onClick: handleAlertDelete,
|
||||
style: {
|
||||
...finalMenuItemStyle,
|
||||
color: Color.BG_CHERRY_400,
|
||||
},
|
||||
danger: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -140,16 +123,21 @@ function AlertActionButtons({
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
<Dropdown trigger={['click']} menu={{ items: menuItems }}>
|
||||
<Tooltip title="More options">
|
||||
<Ellipsis
|
||||
size={16}
|
||||
color={isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}
|
||||
cursor="pointer"
|
||||
className="dropdown-icon"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
<DropdownMenuSimple menu={{ items: menuItems }}>
|
||||
<span className="dropdown-trigger-wrapper">
|
||||
<Tooltip title="More options">
|
||||
<Button
|
||||
type="text"
|
||||
icon={
|
||||
<Ellipsis
|
||||
size={16}
|
||||
color={isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</DropdownMenuSimple>
|
||||
</div>
|
||||
|
||||
<RenameModal
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Input, Slider } from 'antd';
|
||||
import type { SliderRangeProps } from 'antd/es/slider';
|
||||
import { Input } from 'antd';
|
||||
import { Slider } from '@signozhq/ui/slider';
|
||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
|
||||
@@ -88,16 +88,15 @@ export function DurationSection(props: DurationProps): JSX.Element {
|
||||
debouncedFunction(min, max);
|
||||
};
|
||||
|
||||
const onRangeHandler: SliderRangeProps['onChange'] = ([min, max]) => {
|
||||
const onRangeHandler = (value: number | number[]): void => {
|
||||
const [min, max] = value as number[];
|
||||
updateDurationFilter(min.toString(), max.toString());
|
||||
};
|
||||
|
||||
const TipComponent = useCallback((value: undefined | number) => {
|
||||
if (value === undefined) {
|
||||
return <div />;
|
||||
}
|
||||
return <div>{`${value?.toString()}ms`}</div>;
|
||||
}, []);
|
||||
const TipComponent = useCallback(
|
||||
(value: number) => <div>{`${value.toString()}ms`}</div>,
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -123,13 +122,14 @@ export function DurationSection(props: DurationProps): JSX.Element {
|
||||
addonAfter="ms"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="duration-input-slider">
|
||||
<Slider
|
||||
min={0}
|
||||
max={100000}
|
||||
range
|
||||
tooltip={{ formatter: TipComponent }}
|
||||
onChange={([min, max]): void => {
|
||||
onChange={(value): void => {
|
||||
const [min, max] = value as number[];
|
||||
onRangeSliderHandler([String(min), String(max)]);
|
||||
}}
|
||||
onAfterChange={onRangeHandler}
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Form,
|
||||
MenuProps,
|
||||
Space,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import { Button, Divider, Form, Space, Tooltip } from 'antd';
|
||||
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import cx from 'classnames';
|
||||
import { FilterSelect } from 'components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions';
|
||||
@@ -44,16 +37,22 @@ function FunnelStep({
|
||||
const [isAddDetailsModalOpen, setIsAddDetailsModalOpen] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const latencyPointerItems: MenuProps['items'] = LatencyPointers.map(
|
||||
(option) => ({
|
||||
key: option.value,
|
||||
label: option.key,
|
||||
style:
|
||||
option.value === stepData.latency_pointer
|
||||
? { backgroundColor: 'var(--bg-slate-100)' }
|
||||
: {},
|
||||
}),
|
||||
);
|
||||
const latencyPointerItems: MenuItem[] = [
|
||||
{
|
||||
type: 'radio-group',
|
||||
value: stepData.latency_pointer,
|
||||
onChange: (value): void =>
|
||||
onStepChange(index, {
|
||||
latency_pointer: value as FunnelStepData['latency_pointer'],
|
||||
}),
|
||||
children: LatencyPointers.map((option) => ({
|
||||
type: 'radio',
|
||||
key: option.value,
|
||||
label: option.key,
|
||||
value: option.value,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const updatedCurrentQuery = useMemo(
|
||||
() => ({
|
||||
@@ -211,17 +210,18 @@ function FunnelStep({
|
||||
</div>
|
||||
<div className="latency-pointer">
|
||||
<div className="latency-pointer__label">Latency pointer</div>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: latencyPointerItems,
|
||||
onClick: ({ key }): void =>
|
||||
onStepChange(index, {
|
||||
latency_pointer: key as FunnelStepData['latency_pointer'],
|
||||
}),
|
||||
}}
|
||||
trigger={['click']}
|
||||
disabled={!hasEditPermission}
|
||||
>
|
||||
{hasEditPermission ? (
|
||||
<DropdownMenuSimple menu={{ items: latencyPointerItems }}>
|
||||
<Space>
|
||||
{
|
||||
LatencyPointers.find(
|
||||
(option) => option.value === stepData.latency_pointer,
|
||||
)?.key
|
||||
}
|
||||
<ChevronDown size={14} color="var(--bg-vanilla-400)" />
|
||||
</Space>
|
||||
</DropdownMenuSimple>
|
||||
) : (
|
||||
<Space>
|
||||
{
|
||||
LatencyPointers.find(
|
||||
@@ -230,7 +230,7 @@ function FunnelStep({
|
||||
}
|
||||
<ChevronDown size={14} color="var(--bg-vanilla-400)" />
|
||||
</Space>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
12
frontend/src/types/generated/webSettings.ts
Normal file
12
frontend/src/types/generated/webSettings.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM docs/config/web-settings.json */
|
||||
|
||||
export interface WebSettings {
|
||||
appcues: Appcues;
|
||||
posthog: Posthog;
|
||||
}
|
||||
export interface Appcues {
|
||||
enabled: boolean;
|
||||
}
|
||||
export interface Posthog {
|
||||
enabled: boolean;
|
||||
}
|
||||
@@ -15,22 +15,21 @@ type Config struct {
|
||||
// The directory from which to serve the web files.
|
||||
Directory string `mapstructure:"directory"`
|
||||
|
||||
// Settings that are exposed to the web.
|
||||
Settings Settings `mapstructure:"settings"`
|
||||
// Web settings configuration.
|
||||
Settings SettingsConfig `mapstructure:"settings"`
|
||||
}
|
||||
|
||||
// Settings that are exposed to the web.
|
||||
type Settings struct {
|
||||
Posthog Posthog `mapstructure:"posthog"`
|
||||
|
||||
Appcues Appcues `mapstructure:"appcues"`
|
||||
// SettingsConfig holds the configuration for web settings.
|
||||
type SettingsConfig struct {
|
||||
Posthog PosthogConfig `mapstructure:"posthog"`
|
||||
Appcues AppcuesConfig `mapstructure:"appcues"`
|
||||
}
|
||||
|
||||
type Posthog struct {
|
||||
type PosthogConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
}
|
||||
|
||||
type Appcues struct {
|
||||
type AppcuesConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
}
|
||||
|
||||
@@ -43,11 +42,11 @@ func newConfig() factory.Config {
|
||||
Enabled: true,
|
||||
Index: "index.html",
|
||||
Directory: "/etc/signoz/web",
|
||||
Settings: Settings{
|
||||
Posthog: Posthog{
|
||||
Settings: SettingsConfig{
|
||||
Posthog: PosthogConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
Appcues: Appcues{
|
||||
Appcues: AppcuesConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -44,7 +44,8 @@ func New(ctx context.Context, settings factory.ProviderSettings, config web.Conf
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot read %q in web directory", config.Index)
|
||||
}
|
||||
|
||||
settingsJSON, err := json.Marshal(config.Settings)
|
||||
webSettings := web.NewSettings(config)
|
||||
settingsJSON, err := json.Marshal(webSettings)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "cannot marshal web settings to JSON")
|
||||
}
|
||||
|
||||
@@ -118,9 +118,9 @@ func TestServeTemplatedIndex(t *testing.T) {
|
||||
webConfig: web.Config{
|
||||
Index: "valid_template.html",
|
||||
Directory: "testdata",
|
||||
Settings: web.Settings{
|
||||
Posthog: web.Posthog{Enabled: true},
|
||||
Appcues: web.Appcues{Enabled: true},
|
||||
Settings: web.SettingsConfig{
|
||||
Posthog: web.PosthogConfig{Enabled: true},
|
||||
Appcues: web.AppcuesConfig{Enabled: true},
|
||||
},
|
||||
},
|
||||
expected: expectedHTML("/", web.Settings{
|
||||
|
||||
25
pkg/web/settings.go
Normal file
25
pkg/web/settings.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package web
|
||||
|
||||
type Settings struct {
|
||||
Posthog Posthog `json:"posthog" required:"true"`
|
||||
Appcues Appcues `json:"appcues" required:"true"`
|
||||
}
|
||||
|
||||
type Posthog struct {
|
||||
Enabled bool `json:"enabled" required:"true"`
|
||||
}
|
||||
|
||||
type Appcues struct {
|
||||
Enabled bool `json:"enabled" required:"true"`
|
||||
}
|
||||
|
||||
func NewSettings(config Config) Settings {
|
||||
return Settings{
|
||||
Posthog: Posthog{
|
||||
Enabled: config.Settings.Posthog.Enabled,
|
||||
},
|
||||
Appcues: Appcues{
|
||||
Enabled: config.Settings.Appcues.Enabled,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user