mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-20 15:20:31 +01:00
Compare commits
23 Commits
chore/push
...
ns/devenv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18b542b8c5 | ||
|
|
8a8550ac85 | ||
|
|
8629c959f0 | ||
|
|
10760e6e1b | ||
|
|
4f45645b32 | ||
|
|
1417e22ae4 | ||
|
|
3051d442c0 | ||
|
|
ea15ce4e04 | ||
|
|
865a7a5a31 | ||
|
|
de4ca50a40 | ||
|
|
8cabaafc58 | ||
|
|
e9d66b8094 | ||
|
|
26d3d6b1e4 | ||
|
|
36d6debeab | ||
|
|
445b0cace8 | ||
|
|
132f10f8a3 | ||
|
|
14011bc277 | ||
|
|
f17a332c23 | ||
|
|
5ae7a464e6 | ||
|
|
51c3628f6e | ||
|
|
6a69076828 | ||
|
|
edd04e2f07 | ||
|
|
ee734cf78c |
@@ -5,8 +5,8 @@ services:
|
||||
volumes:
|
||||
- ${PWD}/fs/etc/clickhouse-server/config.d/config.xml:/etc/clickhouse-server/config.d/config.xml
|
||||
- ${PWD}/fs/etc/clickhouse-server/users.d/users.xml:/etc/clickhouse-server/users.d/users.xml
|
||||
- ${PWD}/fs/tmp/var/lib/clickhouse/:/var/lib/clickhouse/
|
||||
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
|
||||
- clickhouse_data:/var/lib/clickhouse/
|
||||
ports:
|
||||
- '127.0.0.1:8123:8123'
|
||||
- '127.0.0.1:9000:9000'
|
||||
@@ -69,3 +69,5 @@ services:
|
||||
schema-migrator-sync:
|
||||
condition: service_completed_successfully
|
||||
restart: on-failure
|
||||
volumes:
|
||||
clickhouse_data:
|
||||
9
.github/CODEOWNERS
vendored
9
.github/CODEOWNERS
vendored
@@ -55,7 +55,6 @@
|
||||
/pkg/telemetrymetrics/ @srikanthccv
|
||||
/pkg/telemetrytraces/ @srikanthccv
|
||||
|
||||
|
||||
# Metrics
|
||||
|
||||
/pkg/types/metrictypes/ @srikanthccv
|
||||
@@ -91,6 +90,14 @@
|
||||
# AuthN / AuthZ Owners
|
||||
|
||||
/pkg/authz/ @vikrantgupta25
|
||||
/ee/authz/ @vikrantgupta25
|
||||
/pkg/authn/ @vikrantgupta25
|
||||
/ee/authn/ @vikrantgupta25
|
||||
/pkg/modules/user/ @vikrantgupta25
|
||||
/pkg/modules/session/ @vikrantgupta25
|
||||
/pkg/modules/organization/ @vikrantgupta25
|
||||
/pkg/modules/authdomain/ @vikrantgupta25
|
||||
/pkg/modules/role/ @vikrantgupta25
|
||||
|
||||
# Integration tests
|
||||
|
||||
|
||||
9
.github/pull_request_template.md
vendored
9
.github/pull_request_template.md
vendored
@@ -6,6 +6,15 @@
|
||||
> Why does this change exist?
|
||||
> What problem does it solve, and why is this the right approach?
|
||||
|
||||
|
||||
|
||||
#### Screenshots / Screen Recordings (if applicable)
|
||||
> Include screenshots or screen recordings that clearly show the behavior before the change and the result after the change. This helps reviewers quickly understand the impact and verify the update.
|
||||
|
||||
|
||||
#### Issues closed by this PR
|
||||
> Reference issues using `Closes #issue-number` to enable automatic closure on merge.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Change Type
|
||||
|
||||
7
.github/workflows/commitci.yaml
vendored
7
.github/workflows/commitci.yaml
vendored
@@ -25,3 +25,10 @@ jobs:
|
||||
else
|
||||
echo "No references to 'ee' packages found in 'pkg' directory"
|
||||
fi
|
||||
|
||||
if grep -R --include="*.go" '.*/ee/.*' cmd/community/; then
|
||||
echo "Error: Found references to 'ee' packages in 'cmd/community' directory"
|
||||
exit 1
|
||||
else
|
||||
echo "No references to 'ee' packages found in 'cmd/community' directory"
|
||||
fi
|
||||
|
||||
5
.github/workflows/integrationci.yaml
vendored
5
.github/workflows/integrationci.yaml
vendored
@@ -84,8 +84,11 @@ jobs:
|
||||
sudo rm /etc/apt/sources.list.d/google-chrome.list
|
||||
export CHROMEDRIVER_VERSION=`curl -s https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_${CHROME_MAJOR_VERSION%%.*}`
|
||||
curl -L -O "https://storage.googleapis.com/chrome-for-testing-public/${CHROMEDRIVER_VERSION}/linux64/chromedriver-linux64.zip"
|
||||
unzip chromedriver-linux64.zip && chmod +x chromedriver && sudo mv chromedriver /usr/local/bin
|
||||
unzip chromedriver-linux64.zip
|
||||
chmod +x chromedriver-linux64/chromedriver
|
||||
sudo mv chromedriver-linux64/chromedriver /usr/local/bin/chromedriver
|
||||
chromedriver -version
|
||||
google-chrome-stable --version
|
||||
- name: run
|
||||
run: |
|
||||
cd tests/integration && \
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
|
||||
node_modules
|
||||
|
||||
.vscode
|
||||
!.vscode/settings.json
|
||||
|
||||
deploy/docker/environment_tiny/common_test
|
||||
frontend/node_modules
|
||||
frontend/.pnp
|
||||
@@ -104,7 +107,6 @@ dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
|
||||
62
.vscode/launch.json
vendored
62
.vscode/launch.json
vendored
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "enterprise",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"buildFlags": [
|
||||
"-race",
|
||||
"-ldflags=-X github.com/SigNoz/signoz/pkg/version.version=dev -X github.com/SigNoz/signoz/pkg/version.variant=enterprise -X github.com/SigNoz/signoz/ee/zeus.url=https://api.staging.signoz.cloud"
|
||||
],
|
||||
"program": "${workspaceFolder}/cmd/enterprise/",
|
||||
"args": ["server"],
|
||||
"env": {
|
||||
"SIGNOZ_VERSION_BANNER_ENABLED": "true",
|
||||
"SIGNOZ_INSTRUMENTATION_LOGS_LEVEL": "debug",
|
||||
"SIGNOZ_SQLSTORE_PROVIDER": "sqlite",
|
||||
"SIGNOZ_SQLSTORE_SQLITE_PATH": "${workspaceFolder}/.dev/data/sqlite/enterprise.db",
|
||||
"SIGNOZ_WEB_ENABLED": "false",
|
||||
"SIGNOZ_SQLMIGRATOR_LOCK_INTERVAL": "1m",
|
||||
"SIGNOZ_ALERTMANAGER_PROVIDER": "signoz",
|
||||
"SIGNOZ_TELEMETRYSTORE_PROVIDER": "clickhouse",
|
||||
"SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER": "cluster",
|
||||
"SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN": "tcp://0.0.0.0:9001",
|
||||
"SIGNOZ_PROMETHEUS_ACTIVE__QUERY__TRACKER_ENABLED": "false",
|
||||
"SIGNOZ_EMAILING_ENABLED": "false",
|
||||
"DOT_METRICS_ENABLED": "true",
|
||||
"SIGNOZ_GLOBAL_INGESTION__URL": "http://localhost:3001",
|
||||
"SIGNOZ_TOKENIZER_PROVIDER": "opaque"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "community",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"buildFlags": [
|
||||
"-race",
|
||||
"-ldflags=-X github.com/SigNoz/signoz/pkg/version.version=dev -X github.com/SigNoz/signoz/pkg/version.variant=community -X github.com/SigNoz/signoz/ee/zeus.url=https://api.staging.signoz.cloud"
|
||||
],
|
||||
"program": "${workspaceFolder}/cmd/community/",
|
||||
"args": ["server"],
|
||||
"env": {
|
||||
"SIGNOZ_VERSION_BANNER_ENABLED": "true",
|
||||
"SIGNOZ_INSTRUMENTATION_LOGS_LEVEL": "debug",
|
||||
"SIGNOZ_SQLSTORE_PROVIDER": "sqlite",
|
||||
"SIGNOZ_SQLSTORE_SQLITE_PATH": "${workspaceFolder}/.dev/data/sqlite/community.db",
|
||||
"SIGNOZ_WEB_ENABLED": "false",
|
||||
"SIGNOZ_SQLMIGRATOR_LOCK_INTERVAL": "1m",
|
||||
"SIGNOZ_ALERTMANAGER_PROVIDER": "signoz",
|
||||
"SIGNOZ_TELEMETRYSTORE_PROVIDER": "clickhouse",
|
||||
"SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER": "cluster",
|
||||
"SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN": "tcp://0.0.0.0:9001",
|
||||
"SIGNOZ_PROMETHEUS_ACTIVE__QUERY__TRACKER_ENABLED": "false",
|
||||
"SIGNOZ_EMAILING_ENABLED": "false",
|
||||
"DOT_METRICS_ENABLED": "true",
|
||||
"SIGNOZ_GLOBAL_INGESTION__URL": "http://localhost:3001",
|
||||
"SIGNOZ_TOKENIZER_PROVIDER": "opaque"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,13 +5,14 @@ import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/SigNoz/signoz/cmd"
|
||||
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
|
||||
"github.com/SigNoz/signoz/ee/authz/openfgaschema"
|
||||
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
|
||||
"github.com/SigNoz/signoz/pkg/authz/openfgaschema"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/gateway"
|
||||
"github.com/SigNoz/signoz/pkg/gateway/noopgateway"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
@@ -24,7 +25,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
@@ -57,13 +57,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
// print the version
|
||||
version.Info.PrettyPrint(config.Version)
|
||||
|
||||
// add enterprise sqlstore factories to the community sqlstore factories
|
||||
sqlstoreFactories := signoz.NewSQLStoreProviderFactories()
|
||||
if err := sqlstoreFactories.Add(postgressqlstore.NewFactory(sqlstorehook.NewLoggingFactory())); err != nil {
|
||||
logger.ErrorContext(ctx, "failed to add postgressqlstore factory", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
signoz, err := signoz.New(
|
||||
ctx,
|
||||
config,
|
||||
@@ -90,6 +83,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, _ role.Module, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
|
||||
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)
|
||||
},
|
||||
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||
return noopgateway.NewProviderFactory()
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "failed to create signoz", "error", err)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
|
||||
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
|
||||
"github.com/SigNoz/signoz/ee/authz/openfgaschema"
|
||||
"github.com/SigNoz/signoz/ee/gateway/httpgateway"
|
||||
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
|
||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/gateway"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
@@ -120,6 +122,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, role role.Module, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
|
||||
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, role, queryParser, querier, licensing)
|
||||
},
|
||||
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||
return httpgateway.NewProviderFactory(licensing)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "failed to create signoz", "error", err)
|
||||
|
||||
@@ -176,7 +176,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.107.0
|
||||
image: signoz/signoz:v0.108.0
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.107.0
|
||||
image: signoz/signoz:v0.108.0
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
|
||||
@@ -179,7 +179,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.107.0}
|
||||
image: signoz/signoz:${VERSION:-v0.108.0}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
@@ -111,7 +111,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.107.0}
|
||||
image: signoz/signoz:${VERSION:-v0.108.0}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
@@ -2067,6 +2067,361 @@ paths:
|
||||
summary: Get features
|
||||
tags:
|
||||
- features
|
||||
/api/v2/gateway/ingestion_keys:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns the ingestion keys for a workspace
|
||||
operationId: GetIngestionKeys
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/GatewaytypesGettableIngestionKeys'
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Get ingestion keys for workspace
|
||||
tags:
|
||||
- gateway
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint creates an ingestion key for the workspace
|
||||
operationId: CreateIngestionKey
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GatewaytypesPostableIngestionKey'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/GatewaytypesGettableCreatedIngestionKey'
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Create ingestion key for workspace
|
||||
tags:
|
||||
- gateway
|
||||
/api/v2/gateway/ingestion_keys/{keyId}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: This endpoint deletes an ingestion key for the workspace
|
||||
operationId: DeleteIngestionKey
|
||||
parameters:
|
||||
- in: path
|
||||
name: keyId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Delete ingestion key for workspace
|
||||
tags:
|
||||
- gateway
|
||||
patch:
|
||||
deprecated: false
|
||||
description: This endpoint updates an ingestion key for the workspace
|
||||
operationId: UpdateIngestionKey
|
||||
parameters:
|
||||
- in: path
|
||||
name: keyId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GatewaytypesPostableIngestionKey'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Update ingestion key for workspace
|
||||
tags:
|
||||
- gateway
|
||||
/api/v2/gateway/ingestion_keys/{keyId}/limits:
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint creates an ingestion key limit
|
||||
operationId: CreateIngestionKeyLimit
|
||||
parameters:
|
||||
- in: path
|
||||
name: keyId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GatewaytypesPostableIngestionKeyLimit'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/GatewaytypesGettableCreatedIngestionKeyLimit'
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
description: Created
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Create limit for the ingestion key
|
||||
tags:
|
||||
- gateway
|
||||
/api/v2/gateway/ingestion_keys/limits/{limitId}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: This endpoint deletes an ingestion key limit
|
||||
operationId: DeleteIngestionKeyLimit
|
||||
parameters:
|
||||
- in: path
|
||||
name: limitId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Delete limit for the ingestion key
|
||||
tags:
|
||||
- gateway
|
||||
patch:
|
||||
deprecated: false
|
||||
description: This endpoint updates an ingestion key limit
|
||||
operationId: UpdateIngestionKeyLimit
|
||||
parameters:
|
||||
- in: path
|
||||
name: limitId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GatewaytypesUpdatableIngestionKeyLimit'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Update limit for the ingestion key
|
||||
tags:
|
||||
- gateway
|
||||
/api/v2/gateway/ingestion_keys/search:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns the ingestion keys for a workspace
|
||||
operationId: SearchIngestionKeys
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/GatewaytypesGettableIngestionKeys'
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Search ingestion keys for workspace
|
||||
tags:
|
||||
- gateway
|
||||
/api/v2/metric/alerts:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -3051,6 +3406,160 @@ components:
|
||||
nullable: true
|
||||
type: object
|
||||
type: object
|
||||
GatewaytypesGettableCreatedIngestionKey:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
GatewaytypesGettableCreatedIngestionKeyLimit:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
type: object
|
||||
GatewaytypesGettableIngestionKeys:
|
||||
properties:
|
||||
_pagination:
|
||||
$ref: '#/components/schemas/GatewaytypesPagination'
|
||||
keys:
|
||||
items:
|
||||
$ref: '#/components/schemas/GatewaytypesIngestionKey'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
GatewaytypesIngestionKey:
|
||||
properties:
|
||||
created_at:
|
||||
format: date-time
|
||||
type: string
|
||||
expires_at:
|
||||
format: date-time
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
limits:
|
||||
items:
|
||||
$ref: '#/components/schemas/GatewaytypesLimit'
|
||||
nullable: true
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
tags:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
updated_at:
|
||||
format: date-time
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
workspace_id:
|
||||
type: string
|
||||
type: object
|
||||
GatewaytypesLimit:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/components/schemas/GatewaytypesLimitConfig'
|
||||
created_at:
|
||||
format: date-time
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
key_id:
|
||||
type: string
|
||||
metric:
|
||||
$ref: '#/components/schemas/GatewaytypesLimitMetric'
|
||||
signal:
|
||||
type: string
|
||||
tags:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
updated_at:
|
||||
format: date-time
|
||||
type: string
|
||||
type: object
|
||||
GatewaytypesLimitConfig:
|
||||
properties:
|
||||
day:
|
||||
$ref: '#/components/schemas/GatewaytypesLimitValue'
|
||||
second:
|
||||
$ref: '#/components/schemas/GatewaytypesLimitValue'
|
||||
type: object
|
||||
GatewaytypesLimitMetric:
|
||||
properties:
|
||||
day:
|
||||
$ref: '#/components/schemas/GatewaytypesLimitMetricValue'
|
||||
second:
|
||||
$ref: '#/components/schemas/GatewaytypesLimitMetricValue'
|
||||
type: object
|
||||
GatewaytypesLimitMetricValue:
|
||||
properties:
|
||||
count:
|
||||
format: int64
|
||||
type: integer
|
||||
size:
|
||||
format: int64
|
||||
type: integer
|
||||
type: object
|
||||
GatewaytypesLimitValue:
|
||||
properties:
|
||||
count:
|
||||
format: int64
|
||||
type: integer
|
||||
size:
|
||||
format: int64
|
||||
type: integer
|
||||
type: object
|
||||
GatewaytypesPagination:
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
pages:
|
||||
type: integer
|
||||
per_page:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
type: object
|
||||
GatewaytypesPostableIngestionKey:
|
||||
properties:
|
||||
expires_at:
|
||||
format: date-time
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
tags:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
GatewaytypesPostableIngestionKeyLimit:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/components/schemas/GatewaytypesLimitConfig'
|
||||
signal:
|
||||
type: string
|
||||
tags:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
GatewaytypesUpdatableIngestionKeyLimit:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/components/schemas/GatewaytypesLimitConfig'
|
||||
tags:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
MetricsexplorertypesMetricAlert:
|
||||
properties:
|
||||
alertId:
|
||||
|
||||
282
ee/gateway/httpgateway/provider.go
Normal file
282
ee/gateway/httpgateway/provider.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package httpgateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/gateway"
|
||||
"github.com/SigNoz/signoz/pkg/http/client"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/types/gatewaytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
config gateway.Config
|
||||
httpClient *client.Client
|
||||
licensing licensing.Licensing
|
||||
}
|
||||
|
||||
func NewProviderFactory(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, ps factory.ProviderSettings, c gateway.Config) (gateway.Gateway, error) {
|
||||
return New(ctx, ps, c, licensing)
|
||||
})
|
||||
}
|
||||
|
||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config gateway.Config, licensing licensing.Licensing) (gateway.Gateway, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/gateway/httpgateway")
|
||||
|
||||
httpClient, err := client.New(
|
||||
settings.Logger(),
|
||||
providerSettings.TracerProvider,
|
||||
providerSettings.MeterProvider,
|
||||
client.WithRequestResponseLog(true),
|
||||
client.WithRetryCount(3),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Provider{
|
||||
settings: settings,
|
||||
config: config,
|
||||
httpClient: httpClient,
|
||||
licensing: licensing,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) GetIngestionKeys(ctx context.Context, orgID valuer.UUID, page, perPage int) (*gatewaytypes.GettableIngestionKeys, error) {
|
||||
qParams := url.Values{}
|
||||
qParams.Add("page", strconv.Itoa(page))
|
||||
qParams.Add("per_page", strconv.Itoa(perPage))
|
||||
|
||||
responseBody, err := provider.do(ctx, orgID, http.MethodGet, "/v1/workspaces/me/keys", qParams, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ingestionKeys []gatewaytypes.IngestionKey
|
||||
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &ingestionKeys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pagination gatewaytypes.Pagination
|
||||
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "_pagination").String()), &pagination); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gatewaytypes.GettableIngestionKeys{
|
||||
Keys: ingestionKeys,
|
||||
Pagination: pagination,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) SearchIngestionKeysByName(ctx context.Context, orgID valuer.UUID, name string, page, perPage int) (*gatewaytypes.GettableIngestionKeys, error) {
|
||||
qParams := url.Values{}
|
||||
qParams.Add("name", name)
|
||||
qParams.Add("page", strconv.Itoa(page))
|
||||
qParams.Add("per_page", strconv.Itoa(perPage))
|
||||
|
||||
responseBody, err := provider.do(ctx, orgID, http.MethodGet, "/v1/workspaces/me/keys/search", qParams, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ingestionKeys []gatewaytypes.IngestionKey
|
||||
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &ingestionKeys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pagination gatewaytypes.Pagination
|
||||
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "_pagination").String()), &pagination); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gatewaytypes.GettableIngestionKeys{
|
||||
Keys: ingestionKeys,
|
||||
Pagination: pagination,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) CreateIngestionKey(ctx context.Context, orgID valuer.UUID, name string, tags []string, expiresAt time.Time) (*gatewaytypes.GettableCreatedIngestionKey, error) {
|
||||
requestBody := gatewaytypes.PostableIngestionKey{
|
||||
Name: name,
|
||||
Tags: tags,
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
requestBodyBytes, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseBody, err := provider.do(ctx, orgID, http.MethodPost, "/v1/workspaces/me/keys", nil, requestBodyBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var createdKeyResponse gatewaytypes.GettableCreatedIngestionKey
|
||||
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &createdKeyResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &createdKeyResponse, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) UpdateIngestionKey(ctx context.Context, orgID valuer.UUID, keyID string, name string, tags []string, expiresAt time.Time) error {
|
||||
requestBody := gatewaytypes.PostableIngestionKey{
|
||||
Name: name,
|
||||
Tags: tags,
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
requestBodyBytes, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = provider.do(ctx, orgID, http.MethodPatch, "/v1/workspaces/me/keys/"+keyID, nil, requestBodyBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Provider) DeleteIngestionKey(ctx context.Context, orgID valuer.UUID, keyID string) error {
|
||||
_, err := provider.do(ctx, orgID, http.MethodDelete, "/v1/workspaces/me/keys/"+keyID, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Provider) CreateIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, keyID string, signal string, limitConfig gatewaytypes.LimitConfig, tags []string) (*gatewaytypes.GettableCreatedIngestionKeyLimit, error) {
|
||||
requestBody := gatewaytypes.PostableIngestionKeyLimit{
|
||||
Signal: signal,
|
||||
Config: limitConfig,
|
||||
Tags: tags,
|
||||
}
|
||||
requestBodyBytes, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseBody, err := provider.do(ctx, orgID, http.MethodPost, "/v1/workspaces/me/keys/"+keyID+"/limits", nil, requestBodyBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var createdIngestionKeyLimitResponse gatewaytypes.GettableCreatedIngestionKeyLimit
|
||||
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &createdIngestionKeyLimitResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &createdIngestionKeyLimitResponse, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) UpdateIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, limitID string, limitConfig gatewaytypes.LimitConfig, tags []string) error {
|
||||
requestBody := gatewaytypes.UpdatableIngestionKeyLimit{
|
||||
Config: limitConfig,
|
||||
Tags: tags,
|
||||
}
|
||||
requestBodyBytes, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = provider.do(ctx, orgID, http.MethodPatch, "/v1/workspaces/me/limits/"+limitID, nil, requestBodyBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Provider) DeleteIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, limitID string) error {
|
||||
_, err := provider.do(ctx, orgID, http.MethodDelete, "/v1/workspaces/me/limits/"+limitID, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Provider) do(ctx context.Context, orgID valuer.UUID, method string, path string, queryParams url.Values, body []byte) ([]byte, error) {
|
||||
license, err := provider.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "no valid license found").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
// build url
|
||||
requestURL := provider.config.URL.JoinPath(path)
|
||||
|
||||
// add query params to the url
|
||||
if queryParams != nil {
|
||||
requestURL.RawQuery = queryParams.Encode()
|
||||
}
|
||||
|
||||
// build request
|
||||
request, err := http.NewRequestWithContext(ctx, method, requestURL.String(), bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add headers needed to call gateway
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("X-Signoz-Cloud-Api-Key", license.Key)
|
||||
request.Header.Set("X-Consumer-Username", "lid:00000000-0000-0000-0000-000000000000")
|
||||
request.Header.Set("X-Consumer-Groups", "ns:default")
|
||||
|
||||
// execute request
|
||||
response, err := provider.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read response
|
||||
defer response.Body.Close()
|
||||
responseBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// only 2XX
|
||||
if response.StatusCode/100 == 2 {
|
||||
return responseBody, nil
|
||||
}
|
||||
|
||||
errorMessage := gjson.GetBytes(responseBody, "error").String()
|
||||
if errorMessage == "" {
|
||||
errorMessage = "an unknown error occurred"
|
||||
}
|
||||
|
||||
// return error for non 2XX
|
||||
return nil, provider.errFromStatusCode(response.StatusCode, errorMessage)
|
||||
}
|
||||
|
||||
func (provider *Provider) errFromStatusCode(code int, errorMessage string) error {
|
||||
switch code {
|
||||
case http.StatusBadRequest:
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, errorMessage)
|
||||
case http.StatusUnauthorized:
|
||||
return errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, errorMessage)
|
||||
case http.StatusForbidden:
|
||||
return errors.New(errors.TypeForbidden, errors.CodeForbidden, errorMessage)
|
||||
case http.StatusNotFound:
|
||||
return errors.New(errors.TypeNotFound, errors.CodeNotFound, errorMessage)
|
||||
case http.StatusConflict:
|
||||
return errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, errorMessage)
|
||||
}
|
||||
|
||||
return errors.New(errors.TypeInternal, errors.CodeInternal, errorMessage)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/apis/fields"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
querierAPI "github.com/SigNoz/signoz/pkg/querier"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
@@ -36,6 +37,7 @@ type APIHandlerOptions struct {
|
||||
GatewayUrl string
|
||||
// Querier Influx Interval
|
||||
FluxInterval time.Duration
|
||||
GlobalConfig global.Config
|
||||
}
|
||||
|
||||
type APIHandler struct {
|
||||
|
||||
@@ -76,7 +76,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
|
||||
return
|
||||
}
|
||||
|
||||
ingestionUrl, signozApiUrl, apiErr := ah.getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
|
||||
signozApiUrl, apiErr := ah.getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
|
||||
if apiErr != nil {
|
||||
RespondError(w, basemodel.WrapApiError(
|
||||
apiErr, "couldn't deduce ingestion url and signoz api url",
|
||||
@@ -84,7 +84,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
|
||||
return
|
||||
}
|
||||
|
||||
result.IngestionUrl = ingestionUrl
|
||||
result.IngestionUrl = ah.opts.GlobalConfig.IngestionURL.String()
|
||||
result.SigNozAPIUrl = signozApiUrl
|
||||
|
||||
gatewayUrl := ah.opts.GatewayUrl
|
||||
@@ -186,7 +186,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
|
||||
string, string, *basemodel.ApiError,
|
||||
string, *basemodel.ApiError,
|
||||
) {
|
||||
// TODO: remove this struct from here
|
||||
type deploymentResponse struct {
|
||||
@@ -200,7 +200,7 @@ func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licens
|
||||
|
||||
respBytes, err := ah.Signoz.Zeus.GetDeployment(ctx, licenseKey)
|
||||
if err != nil {
|
||||
return "", "", basemodel.InternalError(fmt.Errorf(
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't query for deployment info: error: %w", err,
|
||||
))
|
||||
}
|
||||
@@ -209,7 +209,7 @@ func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licens
|
||||
|
||||
err = json.Unmarshal(respBytes, resp)
|
||||
if err != nil {
|
||||
return "", "", basemodel.InternalError(fmt.Errorf(
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't unmarshal deployment info response: error: %w", err,
|
||||
))
|
||||
}
|
||||
@@ -219,16 +219,14 @@ func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licens
|
||||
|
||||
if len(regionDns) < 1 || len(deploymentName) < 1 {
|
||||
// Fail early if actual response structure and expectation here ever diverge
|
||||
return "", "", basemodel.InternalError(fmt.Errorf(
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"deployment info response not in expected shape. couldn't determine region dns and deployment name",
|
||||
))
|
||||
}
|
||||
|
||||
ingestionUrl := fmt.Sprintf("https://ingest.%s", regionDns)
|
||||
|
||||
signozApiUrl := fmt.Sprintf("https://%s.%s", deploymentName, regionDns)
|
||||
|
||||
return ingestionUrl, signozApiUrl, nil
|
||||
return signozApiUrl, nil
|
||||
}
|
||||
|
||||
type ingestionKey struct {
|
||||
|
||||
@@ -172,6 +172,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
FluxInterval: config.Querier.FluxInterval,
|
||||
Gateway: gatewayProxy,
|
||||
GatewayUrl: config.Gateway.URL.String(),
|
||||
GlobalConfig: config.Global,
|
||||
}
|
||||
|
||||
apiHandler, err := api.NewAPIHandler(apiOpts, signoz)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
cd frontend && yarn run commitlint --edit $1
|
||||
# Convert $1 to absolute path before cd
|
||||
commit_msg_file="$(realpath "$1")"
|
||||
|
||||
cd frontend && yarn run commitlint "$commit_msg_file"
|
||||
|
||||
branch="$(git rev-parse --abbrev-ref HEAD)"
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
FixedDurationSuggestionOptions,
|
||||
Options,
|
||||
RelativeDurationSuggestionOptions,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import dayjs from 'dayjs';
|
||||
import { isValidShortHandDateTimeFormat } from 'lib/getMinMax';
|
||||
import { defaultTo, isFunction, noop } from 'lodash-es';
|
||||
|
||||
@@ -8,11 +8,11 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||
import { RelativeDurationSuggestionOptions } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import {
|
||||
LexicalContext,
|
||||
Option,
|
||||
RelativeDurationSuggestionOptions,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { Clock, PenLine, TriangleAlertIcon } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
CustomTimeType,
|
||||
LexicalContext,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||
|
||||
@@ -13,7 +13,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import TraceExplorerControls from 'container/TracesExplorer/Controls';
|
||||
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
|
||||
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
|
||||
|
||||
@@ -24,7 +24,7 @@ import { INFRA_MONITORING_K8S_PARAMS_KEYS } from 'container/InfraMonitoringK8s/c
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
|
||||
@@ -5,7 +5,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useMemo } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -12,7 +12,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { OPERATORS, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Formula } from 'container/QueryBuilder/components/Formula';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { memo, useEffect, useMemo, useRef } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -33,6 +33,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
addTraceOperator,
|
||||
panelType,
|
||||
initialDataSource,
|
||||
handleRunQuery,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const containerRef = useRef(null);
|
||||
@@ -157,10 +158,29 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
const tagName = target?.tagName || '';
|
||||
|
||||
const isInputElement =
|
||||
['INPUT', 'TEXTAREA', 'SELECT'].includes(tagName) ||
|
||||
(target?.getAttribute('contenteditable') || '').toLowerCase() === 'true';
|
||||
|
||||
// Allow input elements in qb to run the query when Cmd/Ctrl + Enter is pressed
|
||||
if (isInputElement && (e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleRunQuery();
|
||||
}
|
||||
},
|
||||
[handleRunQuery],
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryBuilderV2Provider>
|
||||
<div className="query-builder-v2">
|
||||
<div className="qb-content-container">
|
||||
<div className="qb-content-container" onKeyDownCapture={handleKeyDown}>
|
||||
{!isMultiQueryAllowed ? (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { get, isEmpty } from 'lodash-es';
|
||||
import { BarChart2, ChevronUp, ExternalLink, ScrollText } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { MetricAggregation } from 'types/api/v5/queryRange';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
@@ -171,6 +171,9 @@ function QueryAddOns({
|
||||
|
||||
const [selectedViews, setSelectedViews] = useState<AddOn[]>([]);
|
||||
|
||||
const initializedRef = useRef(false);
|
||||
const prevAvailableKeysRef = useRef<Set<string> | null>(null);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index,
|
||||
query,
|
||||
@@ -213,23 +216,41 @@ function QueryAddOns({
|
||||
}
|
||||
setAddOns(filteredAddOns);
|
||||
|
||||
const activeAddOnKeys = new Set(
|
||||
Object.entries(ADD_ONS_KEYS_TO_QUERY_PATH)
|
||||
.filter(([, path]) => hasValue(get(query, path)))
|
||||
.map(([key]) => key),
|
||||
);
|
||||
const availableAddOnKeys = new Set(filteredAddOns.map((a) => a.key));
|
||||
const previousKeys = prevAvailableKeysRef.current;
|
||||
const hasAvailabilityItemsChanged =
|
||||
previousKeys !== null &&
|
||||
(previousKeys.size !== availableAddOnKeys.size ||
|
||||
[...availableAddOnKeys].some((key) => !previousKeys.has(key)));
|
||||
prevAvailableKeysRef.current = availableAddOnKeys;
|
||||
|
||||
const availableAddOnKeys = new Set(filteredAddOns.map((addOn) => addOn.key));
|
||||
// Filter and set selected views: add-ons that are both active and available
|
||||
setSelectedViews(
|
||||
filteredAddOns.filter(
|
||||
(addOn) =>
|
||||
activeAddOnKeys.has(addOn.key) && availableAddOnKeys.has(addOn.key),
|
||||
if (!initializedRef.current || hasAvailabilityItemsChanged) {
|
||||
initializedRef.current = true;
|
||||
|
||||
const activeAddOnKeys = new Set(
|
||||
Object.entries(ADD_ONS_KEYS_TO_QUERY_PATH)
|
||||
.filter(([, path]) => hasValue(get(query, path)))
|
||||
.map(([key]) => key),
|
||||
);
|
||||
|
||||
// Initial seeding from query values on mount
|
||||
setSelectedViews(
|
||||
filteredAddOns.filter(
|
||||
(addOn) =>
|
||||
activeAddOnKeys.has(addOn.key) && availableAddOnKeys.has(addOn.key),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedViews((prev) =>
|
||||
prev.filter((view) =>
|
||||
filteredAddOns.some((addOn) => addOn.key === view.key),
|
||||
),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [panelType, isListViewPanel, query]);
|
||||
}, [panelType, isListViewPanel, query, showReduceTo]);
|
||||
|
||||
const handleOptionClick = (e: RadioChangeEvent): void => {
|
||||
if (selectedViews.find((view) => view.key === e.target.value.key)) {
|
||||
|
||||
@@ -1379,8 +1379,6 @@ function QuerySearch({
|
||||
run: (): boolean => {
|
||||
if (onRun && typeof onRun === 'function') {
|
||||
onRun(getCurrentExpression());
|
||||
} else {
|
||||
handleRunQuery();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -410,8 +410,6 @@ function TraceOperatorEditor({
|
||||
run: (): boolean => {
|
||||
if (onRun && typeof onRun === 'function') {
|
||||
onRun(value);
|
||||
} else {
|
||||
handleRunQuery();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -270,44 +270,6 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
await waitFor(() => expect(onRun).toHaveBeenCalled(), { timeout: 2000 });
|
||||
});
|
||||
|
||||
it('calls handleRunQuery when Mod-Enter without onRun', async () => {
|
||||
const mockedHandleRunQuery = handleRunQueryMock as jest.MockedFunction<
|
||||
() => void
|
||||
>;
|
||||
mockedHandleRunQuery.mockClear();
|
||||
|
||||
render(
|
||||
<QuerySearch
|
||||
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
|
||||
queryData={initialQueriesMap.logs.builder.queryData[0]}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Wait for CodeMirror to initialize
|
||||
await waitFor(() => {
|
||||
const editor = document.querySelector(CM_EDITOR_SELECTOR);
|
||||
expect(editor).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
|
||||
await userEvent.click(editor);
|
||||
await userEvent.type(editor, SAMPLE_VALUE_TYPING_COMPLETE);
|
||||
|
||||
// Use fireEvent for keyboard shortcuts as userEvent might not work well with CodeMirror
|
||||
const modKey = navigator.platform.includes('Mac') ? 'metaKey' : 'ctrlKey';
|
||||
fireEvent.keyDown(editor, {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
[modKey]: true,
|
||||
keyCode: 13,
|
||||
});
|
||||
|
||||
await waitFor(() => expect(mockedHandleRunQuery).toHaveBeenCalled(), {
|
||||
timeout: 2000,
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes CodeMirror with expression from queryData.filter.expression on mount', async () => {
|
||||
const testExpression =
|
||||
"http.status_code >= 500 AND service.name = 'frontend'";
|
||||
|
||||
@@ -3,14 +3,21 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import { Having, IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import {
|
||||
Having,
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { UseQueryOperations } from 'types/common/operations.types';
|
||||
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||
|
||||
import { QueryV2 } from '../QueryV2';
|
||||
import { QueryBuilderV2 } from '../../QueryBuilderV2';
|
||||
|
||||
// Local mocks for domain-specific heavy child components
|
||||
jest.mock(
|
||||
@@ -36,16 +43,87 @@ const mockedUseQueryOperations = jest.mocked(
|
||||
useQueryOperations,
|
||||
) as jest.MockedFunction<UseQueryOperations>;
|
||||
|
||||
describe('QueryV2 - base render', () => {
|
||||
describe('QueryBuilderV2 + QueryV2 - base render', () => {
|
||||
let handleRunQueryMock: jest.MockedFunction<() => void>;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockCloneQuery = jest.fn() as jest.MockedFunction<
|
||||
(type: string, q: IBuilderQuery) => void
|
||||
>;
|
||||
handleRunQueryMock = jest.fn() as jest.MockedFunction<() => void>;
|
||||
const baseQuery: IBuilderQuery = {
|
||||
queryName: 'A',
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: '',
|
||||
aggregations: [],
|
||||
timeAggregation: '',
|
||||
spaceAggregation: '',
|
||||
temporality: '',
|
||||
functions: [],
|
||||
filter: undefined,
|
||||
filters: { items: [], op: 'AND' },
|
||||
groupBy: [],
|
||||
expression: '',
|
||||
disabled: false,
|
||||
having: [] as Having[],
|
||||
limit: 10,
|
||||
stepInterval: null,
|
||||
orderBy: [],
|
||||
legend: 'A',
|
||||
};
|
||||
|
||||
const currentQueryObj: Query = {
|
||||
id: 'test',
|
||||
unit: undefined,
|
||||
queryType: EQueryType.CLICKHOUSE,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
queryData: [baseQuery],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
};
|
||||
|
||||
const updateAllQueriesOperators: QueryBuilderContextType['updateAllQueriesOperators'] = (
|
||||
q,
|
||||
) => q;
|
||||
const updateQueriesData: QueryBuilderContextType['updateQueriesData'] = (q) =>
|
||||
q;
|
||||
|
||||
mockedUseQueryBuilder.mockReturnValue(({
|
||||
// Only fields used by QueryV2
|
||||
currentQuery: currentQueryObj,
|
||||
stagedQuery: null,
|
||||
lastUsedQuery: null,
|
||||
setLastUsedQuery: jest.fn(),
|
||||
supersetQuery: currentQueryObj,
|
||||
setSupersetQuery: jest.fn(),
|
||||
initialDataSource: null,
|
||||
panelType: PANEL_TYPES.TABLE,
|
||||
isEnabledQuery: true,
|
||||
handleSetQueryData: jest.fn(),
|
||||
handleSetTraceOperatorData: jest.fn(),
|
||||
handleSetFormulaData: jest.fn(),
|
||||
handleSetQueryItemData: jest.fn(),
|
||||
handleSetConfig: jest.fn(),
|
||||
removeQueryBuilderEntityByIndex: jest.fn(),
|
||||
removeAllQueryBuilderEntities: jest.fn(),
|
||||
removeQueryTypeItemByIndex: jest.fn(),
|
||||
addNewBuilderQuery: jest.fn(),
|
||||
addNewFormula: jest.fn(),
|
||||
removeTraceOperator: jest.fn(),
|
||||
addTraceOperator: jest.fn(),
|
||||
cloneQuery: mockCloneQuery,
|
||||
panelType: null,
|
||||
addNewQueryItem: jest.fn(),
|
||||
redirectWithQueryBuilderData: jest.fn(),
|
||||
handleRunQuery: handleRunQueryMock,
|
||||
resetQuery: jest.fn(),
|
||||
handleOnUnitsChange: jest.fn(),
|
||||
updateAllQueriesOperators,
|
||||
updateQueriesData,
|
||||
initQueryBuilderData: jest.fn(),
|
||||
isStagedQueryUpdated: jest.fn(() => false),
|
||||
isDefaultQuery: jest.fn(() => false),
|
||||
} as unknown) as QueryBuilderContextType);
|
||||
|
||||
mockedUseQueryOperations.mockReturnValue({
|
||||
@@ -71,40 +149,7 @@ describe('QueryV2 - base render', () => {
|
||||
});
|
||||
|
||||
it('renders limit input when dataSource is logs', () => {
|
||||
const baseQuery: IBuilderQuery = {
|
||||
queryName: 'A',
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: '',
|
||||
aggregations: [],
|
||||
timeAggregation: '',
|
||||
spaceAggregation: '',
|
||||
temporality: '',
|
||||
functions: [],
|
||||
filter: undefined,
|
||||
filters: { items: [], op: 'AND' },
|
||||
groupBy: [],
|
||||
expression: '',
|
||||
disabled: false,
|
||||
having: [] as Having[],
|
||||
limit: 10,
|
||||
stepInterval: null,
|
||||
orderBy: [],
|
||||
legend: 'A',
|
||||
};
|
||||
|
||||
render(
|
||||
<QueryV2
|
||||
index={0}
|
||||
isAvailableToDisable
|
||||
query={baseQuery}
|
||||
version="v4"
|
||||
onSignalSourceChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
|
||||
signalSourceChangeEnabled={false}
|
||||
queriesCount={1}
|
||||
showTraceOperator={false}
|
||||
hasTraceOperator={false}
|
||||
/>,
|
||||
);
|
||||
render(<QueryBuilderV2 panelType={PANEL_TYPES.TABLE} version="v4" />);
|
||||
|
||||
// Ensure the Limit add-on input is present and is of type number
|
||||
const limitInput = screen.getByPlaceholderText(
|
||||
@@ -115,4 +160,43 @@ describe('QueryV2 - base render', () => {
|
||||
expect(limitInput).toHaveAttribute('name', 'limit');
|
||||
expect(limitInput).toHaveAttribute('data-testid', 'input-Limit');
|
||||
});
|
||||
|
||||
it('Cmd+Enter on an input triggers handleRunQuery via container handler', async () => {
|
||||
render(<QueryBuilderV2 panelType={PANEL_TYPES.TABLE} version="v4" />);
|
||||
|
||||
const limitInput = screen.getByPlaceholderText('Enter limit');
|
||||
fireEvent.keyDown(limitInput, {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
metaKey: true,
|
||||
});
|
||||
|
||||
expect(handleRunQueryMock).toHaveBeenCalled();
|
||||
|
||||
const legendInput = screen.getByPlaceholderText('Write legend format');
|
||||
fireEvent.keyDown(legendInput, {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
metaKey: true,
|
||||
});
|
||||
|
||||
expect(handleRunQueryMock).toHaveBeenCalled();
|
||||
|
||||
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
|
||||
// Wait for CodeMirror to initialize
|
||||
await waitFor(() => {
|
||||
const editor = document.querySelector(CM_EDITOR_SELECTOR);
|
||||
expect(editor).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
|
||||
await userEvent.click(editor);
|
||||
fireEvent.keyDown(editor, {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
metaKey: true,
|
||||
});
|
||||
|
||||
expect(handleRunQueryMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import { ArrowDown, ArrowUp, X } from 'lucide-react';
|
||||
|
||||
@@ -13,7 +13,7 @@ import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSea
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useQueries } from 'react-query';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
|
||||
@@ -37,10 +37,7 @@ function ThresholdItem({
|
||||
);
|
||||
if (units.length === 0) {
|
||||
component = (
|
||||
<Tooltip
|
||||
trigger="hover"
|
||||
title="Please select a Y-axis unit for the query first"
|
||||
>
|
||||
<Tooltip trigger="hover" title="No compatible units available">
|
||||
<Select
|
||||
placeholder="Unit"
|
||||
value={threshold.unit ? threshold.unit : null}
|
||||
|
||||
@@ -47,9 +47,17 @@ export function getCategoryByOptionId(id: string): string | undefined {
|
||||
}
|
||||
|
||||
export function getCategorySelectOptionByName(
|
||||
name: string,
|
||||
name: string | undefined,
|
||||
): DefaultOptionType[] {
|
||||
if (!name) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const categories = getYAxisCategories(YAxisSource.ALERTS);
|
||||
if (!categories.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (
|
||||
categories
|
||||
.find((category) => category.name === name)
|
||||
|
||||
@@ -15,11 +15,10 @@ 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';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time as TimeV2,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -60,7 +59,7 @@ export interface ChartPreviewProps {
|
||||
query: Query | null;
|
||||
graphType?: PANEL_TYPES;
|
||||
selectedTime?: timePreferenceType;
|
||||
selectedInterval?: Time | TimeV2 | CustomTimeType;
|
||||
selectedInterval?: Time | CustomTimeType;
|
||||
headline?: JSX.Element;
|
||||
alertDef?: AlertDef;
|
||||
userQueryKey?: string;
|
||||
|
||||
@@ -4,3 +4,7 @@
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rule-unit-selector {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,7 @@ import { DefaultOptionType } from 'antd/es/select';
|
||||
import {
|
||||
getCategoryByOptionId,
|
||||
getCategorySelectOptionByName,
|
||||
} from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
} from 'container/CreateAlertV2/AlertCondition/utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
AlertDef,
|
||||
@@ -43,10 +42,10 @@ function RuleOptions({
|
||||
setAlertDef,
|
||||
queryCategory,
|
||||
queryOptions,
|
||||
yAxisUnit,
|
||||
}: RuleOptionsProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const { ruleType } = alertDef;
|
||||
|
||||
@@ -365,11 +364,9 @@ function RuleOptions({
|
||||
</InlineSelect>
|
||||
);
|
||||
|
||||
const selectedCategory = getCategoryByOptionId(currentQuery?.unit || '');
|
||||
const selectedCategory = getCategoryByOptionId(yAxisUnit);
|
||||
|
||||
const categorySelectOptions = getCategorySelectOptionByName(
|
||||
selectedCategory?.name,
|
||||
);
|
||||
const categorySelectOptions = getCategorySelectOptionByName(selectedCategory);
|
||||
|
||||
const step3Label = alertDef.alertType === 'METRIC_BASED_ALERT' ? '3' : '2';
|
||||
|
||||
@@ -402,6 +399,7 @@ function RuleOptions({
|
||||
|
||||
<Form.Item noStyle>
|
||||
<Select
|
||||
className="rule-unit-selector"
|
||||
getPopupContainer={popupContainer}
|
||||
allowClear
|
||||
showSearch
|
||||
@@ -515,5 +513,6 @@ interface RuleOptionsProps {
|
||||
setAlertDef: (a: AlertDef) => void;
|
||||
queryCategory: EQueryType;
|
||||
queryOptions: DefaultOptionType[];
|
||||
yAxisUnit: string;
|
||||
}
|
||||
export default RuleOptions;
|
||||
|
||||
@@ -914,6 +914,7 @@ function FormAlertRules({
|
||||
alertDef={alertDef}
|
||||
setAlertDef={setAlertDef}
|
||||
queryOptions={queryOptions}
|
||||
yAxisUnit={yAxisUnit || ''}
|
||||
/>
|
||||
|
||||
{renderBasicInfo()}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SelectProps } from 'antd';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import getStep from 'lib/getStep';
|
||||
import {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
|
||||
@@ -26,7 +26,7 @@ import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/util
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
|
||||
@@ -26,7 +26,7 @@ import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/util
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
|
||||
@@ -16,7 +16,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { isArray } from 'lodash-es';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
|
||||
@@ -7,7 +7,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useMemo } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -14,7 +14,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import TraceExplorerControls from 'container/TracesExplorer/Controls';
|
||||
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
|
||||
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
|
||||
@@ -23,7 +23,7 @@ import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/util
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
|
||||
@@ -26,7 +26,7 @@ import NodeEvents from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityEv
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
|
||||
@@ -27,7 +27,7 @@ import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/util
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
|
||||
@@ -26,7 +26,7 @@ import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/util
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
@@ -4,7 +4,7 @@ import NewExplorerCTA from 'container/NewExplorerCTA';
|
||||
import { FileText } from 'lucide-react';
|
||||
import { useLocation } from 'react-use';
|
||||
|
||||
import DateTimeSelector from '../TopNav/DateTimeSelection';
|
||||
import DateTimeSelector from '../TopNav/DateTimeSelectionV2';
|
||||
import { Container } from './styles';
|
||||
import { LocalTopNavProps } from './types';
|
||||
|
||||
@@ -37,7 +37,7 @@ function LocalTopNav({
|
||||
{actions}
|
||||
{renderPermissions?.isDateTimeEnabled && (
|
||||
<div>
|
||||
<DateTimeSelector />
|
||||
<DateTimeSelector showAutoRefresh={false} />
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
@@ -4,9 +4,9 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import Controls from 'container/Controls';
|
||||
import Download from 'container/Download/Download';
|
||||
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
|
||||
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
||||
import dayjs from 'dayjs';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { OrderPreferenceItems } from 'pages/Logs/config';
|
||||
import { memo, useMemo } from 'react';
|
||||
@@ -50,7 +50,7 @@ function LogControls(): JSX.Element | null {
|
||||
};
|
||||
|
||||
const handleGoToLatest = (): void => {
|
||||
const { maxTime, minTime } = getMinMax(
|
||||
const { maxTime, minTime } = getMinMaxForSelectedTime(
|
||||
globalTime.selectedTime,
|
||||
globalTime.minTime,
|
||||
globalTime.maxTime,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||
import history from 'lib/history';
|
||||
import { parseQuery } from 'lib/logql';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
@@ -44,7 +44,7 @@ export function useSearchParser(): {
|
||||
search: `?${QueryParams.q}=${updatedQueryString}&${QueryParams.order}=${order}`,
|
||||
});
|
||||
|
||||
const globalTime = getMinMax(selectedTime, minTime, maxTime);
|
||||
const globalTime = getMinMaxForSelectedTime(selectedTime, minTime, maxTime);
|
||||
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_STRING,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time as TimeV2,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { GetMinMaxPayload } from 'lib/getMinMax';
|
||||
|
||||
export const getGlobalTime = (
|
||||
selectedTime: Time | TimeV2 | CustomTimeType,
|
||||
selectedTime: Time | CustomTimeType,
|
||||
globalTime: GetMinMaxPayload,
|
||||
): GetMinMaxPayload | undefined => {
|
||||
if (selectedTime === 'custom') {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
.license-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.license-section-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.license-section-title {
|
||||
color: #fff;
|
||||
font-family: Inter;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.08px;
|
||||
}
|
||||
}
|
||||
|
||||
.license-section-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.license-section-content-item {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--Slate-500, #161922);
|
||||
background: var(--Ink-400, #121317);
|
||||
border-radius: 3px;
|
||||
|
||||
.license-section-content-item-title-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
color: var(--Vanilla-300, #eee);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.license-section-content-item-description {
|
||||
color: var(--Vanilla-400, #c0c1c3);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.license-section {
|
||||
.license-section-header {
|
||||
.license-section-title {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
|
||||
.license-section-content {
|
||||
.license-section-content-item {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.license-section-content-item-title-action {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.license-section-content-item-description {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import './LicenseSection.styles.scss';
|
||||
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Typography } from 'antd';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Copy } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
function LicenseSection(): JSX.Element | null {
|
||||
const { activeLicense } = useAppContext();
|
||||
const { notifications } = useNotifications();
|
||||
const [, handleCopyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const getMaskedKey = (key: string): string => {
|
||||
if (!key || key.length < 4) return key || 'N/A';
|
||||
return `${key.substring(0, 2)}********${key
|
||||
.substring(key.length - 2)
|
||||
.trim()}`;
|
||||
};
|
||||
|
||||
const handleCopyKey = (text: string): void => {
|
||||
handleCopyToClipboard(text);
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
};
|
||||
|
||||
if (!activeLicense?.key) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="license-section">
|
||||
<div className="license-section-header">
|
||||
<div className="license-section-title">License</div>
|
||||
</div>
|
||||
|
||||
<div className="license-section-content">
|
||||
<div className="license-section-content-item">
|
||||
<div className="license-section-content-item-title-action">
|
||||
<span>License key</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Typography.Text code>{getMaskedKey(activeLicense.key)}</Typography.Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-label="Copy license key"
|
||||
data-testid="license-key-copy-btn"
|
||||
onClick={(): void => handleCopyKey(activeLicense.key)}
|
||||
>
|
||||
<Copy size={14} />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="license-section-content-item-description">
|
||||
Your SigNoz license key.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LicenseSection;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './LicenseSection';
|
||||
@@ -1,8 +1,31 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MySettingsContainer from 'container/MySettings';
|
||||
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from 'tests/test-utils';
|
||||
|
||||
const toggleThemeFunction = jest.fn();
|
||||
const logEventFunction = jest.fn();
|
||||
const copyToClipboardFn = jest.fn();
|
||||
const editUserFn = jest.fn();
|
||||
|
||||
jest.mock('react-use', () => ({
|
||||
__esModule: true,
|
||||
useCopyToClipboard: (): [unknown, (text: string) => void] => [
|
||||
null,
|
||||
copyToClipboardFn,
|
||||
],
|
||||
}));
|
||||
|
||||
jest.mock('api/v1/user/id/update', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: unknown[]): Promise<unknown> => editUserFn(...args),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
__esModule: true,
|
||||
@@ -44,6 +67,7 @@ const PASSWORD_VALIDATION_MESSAGE_TEST_ID = 'password-validation-message';
|
||||
describe('MySettings Flows', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
editUserFn.mockResolvedValue({});
|
||||
render(<MySettingsContainer />);
|
||||
});
|
||||
|
||||
@@ -215,4 +239,71 @@ describe('MySettings Flows', () => {
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('License section', () => {
|
||||
it('Should render license section content when license key exists', () => {
|
||||
expect(screen.getByText('License')).toBeInTheDocument();
|
||||
expect(screen.getByText('License key')).toBeInTheDocument();
|
||||
expect(screen.getByText('Your SigNoz license key.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should not render license section when license key is missing', () => {
|
||||
const { container } = render(<MySettingsContainer />, undefined, {
|
||||
appContextOverrides: {
|
||||
activeLicense: null,
|
||||
},
|
||||
});
|
||||
|
||||
const scoped = within(container);
|
||||
expect(scoped.queryByText('License')).not.toBeInTheDocument();
|
||||
expect(scoped.queryByText('License key')).not.toBeInTheDocument();
|
||||
expect(
|
||||
scoped.queryByText('Your SigNoz license key.'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should mask license key in the UI', () => {
|
||||
const { container } = render(<MySettingsContainer />, undefined, {
|
||||
appContextOverrides: {
|
||||
activeLicense: {
|
||||
key: 'abcd',
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
|
||||
expect(within(container).getByText('ab********cd')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should not mask license key if it is too short', () => {
|
||||
const { container } = render(<MySettingsContainer />, undefined, {
|
||||
appContextOverrides: {
|
||||
activeLicense: {
|
||||
key: 'abc',
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
|
||||
expect(within(container).getByText('abc')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should copy license key and show success toast', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(<MySettingsContainer />, undefined, {
|
||||
appContextOverrides: {
|
||||
activeLicense: {
|
||||
key: 'test-license-key-12345',
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
|
||||
await user.click(within(container).getByTestId('license-key-copy-btn'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(copyToClipboardFn).toHaveBeenCalledWith('test-license-key-12345');
|
||||
expect(successNotification).toHaveBeenCalledWith({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useMutation } from 'react-query';
|
||||
import { UserPreference } from 'types/api/preferences/preference';
|
||||
import { showErrorNotification } from 'utils/error';
|
||||
|
||||
import LicenseSection from './LicenseSection';
|
||||
import TimezoneAdaptation from './TimezoneAdaptation/TimezoneAdaptation';
|
||||
import UserInfo from './UserInfo';
|
||||
|
||||
@@ -230,6 +231,8 @@ function MySettings(): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LicenseSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
|
||||
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
|
||||
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import './styles.scss';
|
||||
|
||||
import { RelativeDurationOptions } from 'container/TopNav/DateTimeSelection/config';
|
||||
import { RelativeDurationOptions } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import { useState } from 'react';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import './styles.scss';
|
||||
|
||||
import { RelativeDurationOptions } from 'container/TopNav/DateTimeSelection/config';
|
||||
import { RelativeDurationOptions } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import { useState } from 'react';
|
||||
import { PipelineData } from 'types/api/pipeline/def';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
initialQueriesMap,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import './styles.scss';
|
||||
|
||||
import { Select } from 'antd';
|
||||
import {
|
||||
RelativeDurationOptions,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelection/config';
|
||||
import { RelativeDurationOptions } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import LogsCountInInterval from './components/LogsCountInInterval';
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
initialQueriesMap,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
@@ -9,7 +9,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ServiceDataProps } from 'api/metrics/getTopLevelOperations';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time as TimeV2,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
@@ -26,7 +25,7 @@ export interface ServiceMetricsTableProps {
|
||||
|
||||
export interface GetQueryRangeRequestDataProps {
|
||||
topLevelOperations: [keyof ServiceDataProps, string[]][];
|
||||
globalSelectedInterval: Time | TimeV2 | CustomTimeType;
|
||||
globalSelectedInterval: Time | CustomTimeType;
|
||||
dotMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,17 @@ interface AttributeRecord {
|
||||
interface AttributeActionsProps {
|
||||
record: AttributeRecord;
|
||||
isPinned?: boolean;
|
||||
onTogglePin: (fieldKey: string) => void;
|
||||
onTogglePin?: (fieldKey: string) => void;
|
||||
showPinned?: boolean;
|
||||
showCopyOptions?: boolean;
|
||||
}
|
||||
|
||||
export default function AttributeActions({
|
||||
record,
|
||||
isPinned,
|
||||
onTogglePin,
|
||||
showPinned = true,
|
||||
showCopyOptions = true,
|
||||
}: AttributeActionsProps): JSX.Element {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [isFilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
|
||||
@@ -91,7 +95,7 @@ export default function AttributeActions({
|
||||
}, [onCopyFieldValue, textToCopy]);
|
||||
|
||||
const handleTogglePin = useCallback((): void => {
|
||||
onTogglePin(record.field);
|
||||
onTogglePin?.(record.field);
|
||||
}, [onTogglePin, record.field]);
|
||||
|
||||
const moreActionsContent = (
|
||||
@@ -105,35 +109,41 @@ export default function AttributeActions({
|
||||
>
|
||||
Group By Attribute
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Copy size={14} />}
|
||||
onClick={handleCopyFieldName}
|
||||
block
|
||||
>
|
||||
Copy Field Name
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Copy size={14} />}
|
||||
onClick={handleCopyFieldValue}
|
||||
block
|
||||
>
|
||||
Copy Field Value
|
||||
</Button>
|
||||
{showCopyOptions && (
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Copy size={14} />}
|
||||
onClick={handleCopyFieldName}
|
||||
block
|
||||
>
|
||||
Copy Field Name
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Copy size={14} />}
|
||||
onClick={handleCopyFieldValue}
|
||||
block
|
||||
>
|
||||
Copy Field Value
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx('action-btn', { 'action-btn--is-open': isOpen })}>
|
||||
<Tooltip title={isPinned ? 'Unpin attribute' : 'Pin attribute'}>
|
||||
<Button
|
||||
className={`filter-btn periscope-btn ${isPinned ? 'pinned' : ''}`}
|
||||
aria-label={isPinned ? 'Unpin attribute' : 'Pin attribute'}
|
||||
icon={<Pin size={14} fill={isPinned ? 'currentColor' : 'none'} />}
|
||||
onClick={handleTogglePin}
|
||||
/>
|
||||
</Tooltip>
|
||||
{showPinned && (
|
||||
<Tooltip title={isPinned ? 'Unpin attribute' : 'Pin attribute'}>
|
||||
<Button
|
||||
className={`filter-btn periscope-btn ${isPinned ? 'pinned' : ''}`}
|
||||
aria-label={isPinned ? 'Unpin attribute' : 'Pin attribute'}
|
||||
icon={<Pin size={14} fill={isPinned ? 'currentColor' : 'none'} />}
|
||||
onClick={handleTogglePin}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Filter for value">
|
||||
<Button
|
||||
className="filter-btn periscope-btn"
|
||||
@@ -184,4 +194,7 @@ export default function AttributeActions({
|
||||
|
||||
AttributeActions.defaultProps = {
|
||||
isPinned: false,
|
||||
showPinned: true,
|
||||
showCopyOptions: true,
|
||||
onTogglePin: undefined,
|
||||
};
|
||||
|
||||
@@ -47,15 +47,56 @@
|
||||
.description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 10px 12px;
|
||||
padding: 10px 0px;
|
||||
|
||||
.item {
|
||||
padding: 8px 12px;
|
||||
&,
|
||||
.attribute-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
position: relative; // ensure absolutely-positioned children anchor to the row
|
||||
}
|
||||
|
||||
// Show attribute actions on hover for hardcoded rows
|
||||
.attribute-actions-wrapper {
|
||||
display: none;
|
||||
gap: 8px;
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
|
||||
// style the action button group
|
||||
.action-btn {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.filter-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-slate-400);
|
||||
padding: 4px;
|
||||
gap: 3px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-slate-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--bg-slate-500);
|
||||
.attribute-actions-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.span-name-wrapper {
|
||||
@@ -646,6 +687,29 @@
|
||||
|
||||
.description {
|
||||
.item {
|
||||
.attribute-actions-wrapper {
|
||||
display: none;
|
||||
gap: 8px;
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
|
||||
.filter-btn {
|
||||
background: var(--bg-vanilla-200);
|
||||
&:hover {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
.attribute-actions-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.span-name-wrapper {
|
||||
.span-percentile-value-container {
|
||||
&.span-percentile-value-container-open {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { USER_PREFERENCES } from 'constants/userPreferences';
|
||||
import AttributeActions from 'container/SpanDetailsDrawer/Attributes/AttributeActions';
|
||||
import dayjs from 'dayjs';
|
||||
import useClickOutside from 'hooks/useClickOutside';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
@@ -103,6 +104,10 @@ interface IResourceAttribute {
|
||||
const DEFAULT_RESOURCE_ATTRIBUTES = {
|
||||
serviceName: 'service.name',
|
||||
name: 'name',
|
||||
spanId: 'span_id',
|
||||
spanKind: 'kind_string',
|
||||
statusCodeString: 'status_code_string',
|
||||
statusMessage: 'status_message',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
@@ -835,6 +840,16 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
|
||||
{selectedSpan.spanId}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="attribute-actions-wrapper">
|
||||
<AttributeActions
|
||||
record={{
|
||||
field: DEFAULT_RESOURCE_ATTRIBUTES.spanId,
|
||||
value: selectedSpan.spanId,
|
||||
}}
|
||||
showPinned={false}
|
||||
showCopyOptions={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="item">
|
||||
<Typography.Text className="attribute-key">start time</Typography.Text>
|
||||
@@ -863,6 +878,16 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="attribute-actions-wrapper">
|
||||
<AttributeActions
|
||||
record={{
|
||||
field: DEFAULT_RESOURCE_ATTRIBUTES.serviceName,
|
||||
value: selectedSpan.serviceName,
|
||||
}}
|
||||
showPinned={false}
|
||||
showCopyOptions={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="item">
|
||||
@@ -872,6 +897,16 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
|
||||
{selectedSpan.spanKind}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="attribute-actions-wrapper">
|
||||
<AttributeActions
|
||||
record={{
|
||||
field: DEFAULT_RESOURCE_ATTRIBUTES.spanKind,
|
||||
value: selectedSpan.spanKind,
|
||||
}}
|
||||
showPinned={false}
|
||||
showCopyOptions={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="item">
|
||||
<Typography.Text className="attribute-key">
|
||||
@@ -882,6 +917,16 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
|
||||
{selectedSpan.statusCodeString}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="attribute-actions-wrapper">
|
||||
<AttributeActions
|
||||
record={{
|
||||
field: DEFAULT_RESOURCE_ATTRIBUTES.statusCodeString,
|
||||
value: selectedSpan.statusCodeString,
|
||||
}}
|
||||
showPinned={false}
|
||||
showCopyOptions={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedSpan.statusMessage && (
|
||||
@@ -891,6 +936,16 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
|
||||
attributeValue={selectedSpan.statusMessage}
|
||||
onExpand={showStatusMessageModal}
|
||||
/>
|
||||
<div className="attribute-actions-wrapper">
|
||||
<AttributeActions
|
||||
record={{
|
||||
field: DEFAULT_RESOURCE_ATTRIBUTES.statusMessage,
|
||||
value: selectedSpan.statusMessage,
|
||||
}}
|
||||
showPinned={false}
|
||||
showCopyOptions={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="item">
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'tests/test-utils';
|
||||
|
||||
import AttributeActions from '../Attributes/AttributeActions';
|
||||
|
||||
// Mock only Popover from antd to simplify hover/open behavior while keeping other components real
|
||||
jest.mock('antd', () => {
|
||||
const actual = jest.requireActual('antd');
|
||||
const MockPopover = ({
|
||||
content,
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
...rest
|
||||
}: any): JSX.Element => (
|
||||
<div
|
||||
data-testid="mock-popover-wrapper"
|
||||
onMouseEnter={(): void => onOpenChange?.(true)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
{open ? <div data-testid="mock-popover-content">{content}</div> : null}
|
||||
</div>
|
||||
);
|
||||
return { ...actual, Popover: MockPopover };
|
||||
});
|
||||
|
||||
// Mock getAggregateKeys API used inside useTraceActions to resolve autocomplete keys
|
||||
jest.mock('api/queryBuilder/getAttributeKeys', () => ({
|
||||
getAggregateKeys: jest.fn().mockResolvedValue({
|
||||
payload: {
|
||||
attributeKeys: [
|
||||
{
|
||||
key: 'http.method',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const record = { field: 'http.method', value: 'GET' };
|
||||
|
||||
describe('AttributeActions (unit)', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders core action buttons (pin, filter in/out, more)', async () => {
|
||||
render(<AttributeActions record={record} isPinned={false} />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Pin attribute' }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Filter for value' }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Filter out value' }),
|
||||
).toBeInTheDocument();
|
||||
// more actions (ellipsis) button
|
||||
expect(
|
||||
document.querySelector('.lucide-ellipsis')?.closest('button'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies "Filter for" and calls redirectWithQueryBuilderData with correct query', async () => {
|
||||
const redirectWithQueryBuilderData = jest.fn();
|
||||
const currentQuery = {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: { key: 'signoz_span_duration' },
|
||||
filters: { items: [], op: 'AND' },
|
||||
filter: { expression: '' },
|
||||
groupBy: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
|
||||
render(<AttributeActions record={record} />, undefined, {
|
||||
queryBuilderOverrides: { currentQuery, redirectWithQueryBuilderData },
|
||||
});
|
||||
|
||||
const filterForBtn = screen.getByRole('button', { name: 'Filter for value' });
|
||||
|
||||
await userEvent.click(filterForBtn);
|
||||
await waitFor(() => {
|
||||
expect(redirectWithQueryBuilderData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
builder: expect.objectContaining({
|
||||
queryData: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
filters: expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: 'http.method' }),
|
||||
op: '=',
|
||||
value: 'GET',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
{},
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('applies "Filter out" and calls redirectWithQueryBuilderData with correct query', async () => {
|
||||
const redirectWithQueryBuilderData = jest.fn();
|
||||
const currentQuery = {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: { key: 'signoz_span_duration' },
|
||||
filters: { items: [], op: 'AND' },
|
||||
filter: { expression: '' },
|
||||
groupBy: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
|
||||
render(<AttributeActions record={record} />, undefined, {
|
||||
queryBuilderOverrides: { currentQuery, redirectWithQueryBuilderData },
|
||||
});
|
||||
|
||||
const filterOutBtn = screen.getByRole('button', { name: 'Filter out value' });
|
||||
|
||||
await userEvent.click(filterOutBtn);
|
||||
await waitFor(() => {
|
||||
expect(redirectWithQueryBuilderData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
builder: expect.objectContaining({
|
||||
queryData: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
filters: expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: 'http.method' }),
|
||||
op: '!=',
|
||||
value: 'GET',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
{},
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('opens more actions on hover and calls Group By handler; closes after click', async () => {
|
||||
const redirectWithQueryBuilderData = jest.fn();
|
||||
const currentQuery = {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: { key: 'signoz_span_duration' },
|
||||
filters: { items: [], op: 'AND' },
|
||||
filter: { expression: '' },
|
||||
groupBy: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
render(<AttributeActions record={record} />, undefined, {
|
||||
queryBuilderOverrides: { currentQuery, redirectWithQueryBuilderData },
|
||||
});
|
||||
|
||||
const ellipsisBtn = document
|
||||
.querySelector('.lucide-ellipsis')
|
||||
?.closest('button') as HTMLElement;
|
||||
expect(ellipsisBtn).toBeInTheDocument();
|
||||
|
||||
// hover to trigger Popover open via mock
|
||||
fireEvent.mouseEnter(ellipsisBtn.parentElement as Element);
|
||||
|
||||
// content appears
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Group By Attribute')).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('Group By Attribute'));
|
||||
await waitFor(() => {
|
||||
expect(redirectWithQueryBuilderData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
builder: expect.objectContaining({
|
||||
queryData: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
groupBy: expect.arrayContaining([
|
||||
expect.objectContaining({ key: 'http.method' }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
{},
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
|
||||
// After clicking group by, popover should close
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByTestId('mock-popover-content')).not.toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('hides pin button when showPinned=false', async () => {
|
||||
render(<AttributeActions record={record} showPinned={false} />);
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /pin attribute/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides copy options when showCopyOptions=false', async () => {
|
||||
render(<AttributeActions record={record} showCopyOptions={false} />);
|
||||
const ellipsisBtn = document
|
||||
.querySelector('.lucide-ellipsis')
|
||||
?.closest('button') as HTMLElement;
|
||||
fireEvent.mouseEnter(ellipsisBtn.parentElement as Element);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText('Copy Field Name')).not.toBeInTheDocument(),
|
||||
);
|
||||
expect(screen.queryByText('Copy Field Value')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@ import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import EmptyMetricsSearch from 'container/MetricsExplorer/Explorer/EmptyMetricsSearch';
|
||||
import { MetricsLoading } from 'container/MetricsExplorer/MetricsLoading/MetricsLoading';
|
||||
import NoLogs from 'container/NoLogs/NoLogs';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import GetMinMax, { GetMinMaxPayload } from 'lib/getMinMax';
|
||||
|
||||
import { Time } from '../DateTimeSelection/config';
|
||||
import { CustomTimeType, Time as TimeV2 } from '../DateTimeSelectionV2/config';
|
||||
|
||||
export const options: IOptions[] = [
|
||||
{
|
||||
label: 'off',
|
||||
key: 'off',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '5s',
|
||||
key: '5s',
|
||||
value: 5000,
|
||||
},
|
||||
{
|
||||
label: '10s',
|
||||
key: '10s',
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
label: '30s',
|
||||
key: '30s',
|
||||
value: 30000,
|
||||
},
|
||||
{
|
||||
label: '1m',
|
||||
key: '1m',
|
||||
value: 60000,
|
||||
},
|
||||
{
|
||||
label: '5m',
|
||||
key: '5m',
|
||||
value: 300000,
|
||||
},
|
||||
{
|
||||
label: '10m',
|
||||
key: '10m',
|
||||
value: 600000,
|
||||
},
|
||||
{
|
||||
label: '30m',
|
||||
key: '30m',
|
||||
value: 1800000,
|
||||
},
|
||||
{
|
||||
label: '1h',
|
||||
key: '1h',
|
||||
value: 3600000,
|
||||
},
|
||||
{
|
||||
label: '2h',
|
||||
key: '2h',
|
||||
value: 7200000,
|
||||
},
|
||||
{
|
||||
label: '1d',
|
||||
key: '1d',
|
||||
value: 86400000,
|
||||
},
|
||||
];
|
||||
|
||||
export interface IOptions {
|
||||
label: string;
|
||||
key: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const getMinMax = (
|
||||
selectedTime: Time | TimeV2 | CustomTimeType,
|
||||
minTime: number,
|
||||
maxTime: number,
|
||||
): GetMinMaxPayload =>
|
||||
selectedTime !== 'custom'
|
||||
? GetMinMax(selectedTime)
|
||||
: GetMinMax(selectedTime, [minTime, maxTime]);
|
||||
@@ -1,202 +0,0 @@
|
||||
import { CaretDownFilled } from '@ant-design/icons';
|
||||
import {
|
||||
Checkbox,
|
||||
Divider,
|
||||
Popover,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Space,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
|
||||
import get from 'api/browser/localstorage/get';
|
||||
import set from 'api/browser/localstorage/set';
|
||||
import { DASHBOARD_TIME_IN_DURATION } from 'constants/app';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import _omit from 'lodash-es/omit';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useInterval } from 'react-use';
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
UPDATE_AUTO_REFRESH_INTERVAL,
|
||||
UPDATE_TIME_INTERVAL,
|
||||
} from 'types/actions/globalTime';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { getMinMax, options } from './config';
|
||||
import { ButtonContainer, Container } from './styles';
|
||||
|
||||
function AutoRefresh({
|
||||
disabled = false,
|
||||
showAutoRefreshBtnPrimary = true,
|
||||
}: AutoRefreshProps): JSX.Element {
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const isDisabled = useMemo(
|
||||
() =>
|
||||
disabled ||
|
||||
globalTime.isAutoRefreshDisabled ||
|
||||
globalTime.selectedTime === 'custom',
|
||||
[globalTime.isAutoRefreshDisabled, disabled, globalTime.selectedTime],
|
||||
);
|
||||
|
||||
const localStorageData = JSON.parse(get(DASHBOARD_TIME_IN_DURATION) || '{}');
|
||||
|
||||
const localStorageValue = useMemo(() => localStorageData[pathname], [
|
||||
pathname,
|
||||
localStorageData,
|
||||
]);
|
||||
|
||||
const [isAutoRefreshEnabled, setIsAutoRefreshfreshEnabled] = useState<boolean>(
|
||||
Boolean(localStorageValue),
|
||||
);
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
useEffect(() => {
|
||||
const isAutoRefreshEnabled = Boolean(localStorageValue);
|
||||
dispatch({
|
||||
type: UPDATE_AUTO_REFRESH_INTERVAL,
|
||||
payload: localStorageValue,
|
||||
});
|
||||
setIsAutoRefreshfreshEnabled(isAutoRefreshEnabled);
|
||||
}, [localStorageValue, dispatch]);
|
||||
|
||||
const params = useUrlQuery();
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState<string>(
|
||||
localStorageValue || options[0].key,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedOption(localStorageValue || options[0].key);
|
||||
}, [localStorageValue, params]);
|
||||
|
||||
const getOption = useMemo(
|
||||
() => options.find((option) => option.key === selectedOption),
|
||||
[selectedOption],
|
||||
);
|
||||
|
||||
useInterval(() => {
|
||||
const selectedValue = getOption?.value;
|
||||
|
||||
if (isDisabled || !isAutoRefreshEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedOption !== 'off' && selectedValue) {
|
||||
const { maxTime, minTime } = getMinMax(
|
||||
globalTime.selectedTime,
|
||||
globalTime.minTime,
|
||||
globalTime.maxTime,
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_TIME_INTERVAL,
|
||||
payload: {
|
||||
maxTime,
|
||||
minTime,
|
||||
selectedTime: globalTime.selectedTime,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, getOption?.value || 0);
|
||||
|
||||
const onChangeHandler = useCallback(
|
||||
(event: RadioChangeEvent) => {
|
||||
const selectedValue = event.target.value;
|
||||
setSelectedOption(selectedValue);
|
||||
params.set(DASHBOARD_TIME_IN_DURATION, selectedValue);
|
||||
set(
|
||||
DASHBOARD_TIME_IN_DURATION,
|
||||
JSON.stringify({ ...localStorageData, [pathname]: selectedValue }),
|
||||
);
|
||||
setIsAutoRefreshfreshEnabled(true);
|
||||
},
|
||||
[params, pathname, localStorageData],
|
||||
);
|
||||
|
||||
const onChangeAutoRefreshHandler = useCallback(
|
||||
(event: CheckboxChangeEvent) => {
|
||||
const { checked } = event.target;
|
||||
if (!checked) {
|
||||
// remove the path from localstorage
|
||||
set(
|
||||
DASHBOARD_TIME_IN_DURATION,
|
||||
JSON.stringify(_omit(localStorageData, pathname)),
|
||||
);
|
||||
}
|
||||
setIsAutoRefreshfreshEnabled(checked);
|
||||
},
|
||||
[localStorageData, pathname],
|
||||
);
|
||||
|
||||
if (globalTime.selectedTime === 'custom') {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
getPopupContainer={popupContainer}
|
||||
placement="bottomLeft"
|
||||
trigger={['click']}
|
||||
content={
|
||||
<Container>
|
||||
<Checkbox
|
||||
onChange={onChangeAutoRefreshHandler}
|
||||
checked={isAutoRefreshEnabled}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Auto Refresh
|
||||
</Checkbox>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Typography.Paragraph disabled={isDisabled}>
|
||||
Refresh Interval
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Radio.Group onChange={onChangeHandler} value={selectedOption}>
|
||||
<Space direction="vertical">
|
||||
{options
|
||||
.filter((e) => e.label !== 'off')
|
||||
.map((option) => (
|
||||
<Radio disabled={isDisabled} key={option.key} value={option.key}>
|
||||
{option.label}
|
||||
</Radio>
|
||||
))}
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</Container>
|
||||
}
|
||||
>
|
||||
<ButtonContainer
|
||||
title="Set auto refresh"
|
||||
type={showAutoRefreshBtnPrimary ? 'primary' : 'default'}
|
||||
>
|
||||
<CaretDownFilled />
|
||||
</ButtonContainer>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
interface AutoRefreshProps {
|
||||
disabled?: boolean;
|
||||
showAutoRefreshBtnPrimary?: boolean;
|
||||
}
|
||||
|
||||
AutoRefresh.defaultProps = {
|
||||
disabled: false,
|
||||
showAutoRefreshBtnPrimary: true,
|
||||
};
|
||||
|
||||
export default AutoRefresh;
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Button } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
min-width: 8rem;
|
||||
`;
|
||||
|
||||
export const ButtonContainer = styled(Button)`
|
||||
&&& {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
`;
|
||||
@@ -1,9 +1,10 @@
|
||||
import GetMinMax, { GetMinMaxPayload } from 'lib/getMinMax';
|
||||
export interface IOptions {
|
||||
label: string;
|
||||
key: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
import { Time } from '../DateTimeSelection/config';
|
||||
import { CustomTimeType, Time as TimeV2 } from '../DateTimeSelectionV2/config';
|
||||
|
||||
export const options: IOptions[] = [
|
||||
export const refreshIntervalOptions: IOptions[] = [
|
||||
{
|
||||
label: 'off',
|
||||
key: 'off',
|
||||
@@ -60,18 +61,3 @@ export const options: IOptions[] = [
|
||||
value: 86400000,
|
||||
},
|
||||
];
|
||||
|
||||
export interface IOptions {
|
||||
label: string;
|
||||
key: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const getMinMax = (
|
||||
selectedTime: Time | TimeV2 | CustomTimeType,
|
||||
minTime: number,
|
||||
maxTime: number,
|
||||
): GetMinMaxPayload =>
|
||||
selectedTime !== 'custom'
|
||||
? GetMinMax(selectedTime)
|
||||
: GetMinMax(selectedTime, [minTime, maxTime]);
|
||||
@@ -7,6 +7,7 @@ import get from 'api/browser/localstorage/get';
|
||||
import set from 'api/browser/localstorage/set';
|
||||
import { DASHBOARD_TIME_IN_DURATION } from 'constants/app';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||
import _omit from 'lodash-es/omit';
|
||||
import { Check } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { getMinMax, options } from './config';
|
||||
import { refreshIntervalOptions } from './constants';
|
||||
import { ButtonContainer } from './styles';
|
||||
|
||||
const DEFAULT_REFRESH_INTERVAL = '30s';
|
||||
@@ -70,20 +71,23 @@ function AutoRefresh({
|
||||
const params = useUrlQuery();
|
||||
|
||||
const defaultOption = useMemo(
|
||||
() => options.find((option) => option.key === DEFAULT_REFRESH_INTERVAL),
|
||||
() =>
|
||||
refreshIntervalOptions.find(
|
||||
(option) => option.key === DEFAULT_REFRESH_INTERVAL,
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState<string>(
|
||||
localStorageValue || options[0].key,
|
||||
localStorageValue || refreshIntervalOptions[0].key,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedOption(localStorageValue || options[0].key);
|
||||
setSelectedOption(localStorageValue || refreshIntervalOptions[0].key);
|
||||
}, [localStorageValue, params, defaultOption]);
|
||||
|
||||
const getOption = useMemo(
|
||||
() => options.find((option) => option.key === selectedOption),
|
||||
() => refreshIntervalOptions.find((option) => option.key === selectedOption),
|
||||
[selectedOption],
|
||||
);
|
||||
|
||||
@@ -95,7 +99,7 @@ function AutoRefresh({
|
||||
}
|
||||
|
||||
if (selectedOption !== 'off' && selectedValue) {
|
||||
const { maxTime, minTime } = getMinMax(
|
||||
const { maxTime, minTime } = getMinMaxForSelectedTime(
|
||||
globalTime.selectedTime,
|
||||
globalTime.minTime,
|
||||
globalTime.maxTime,
|
||||
@@ -175,7 +179,7 @@ function AutoRefresh({
|
||||
>
|
||||
Refresh Interval
|
||||
</Typography.Paragraph>
|
||||
{options
|
||||
{refreshIntervalOptions
|
||||
.filter((e) => e.label !== 'off')
|
||||
.map((option) => (
|
||||
<Button
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.date-time-selection-container {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { RefreshTextContainer, Typography } from './styles';
|
||||
|
||||
function RefreshText({
|
||||
onLastRefreshHandler,
|
||||
refreshButtonHidden,
|
||||
}: RefreshTextProps): JSX.Element {
|
||||
const [refreshText, setRefreshText] = useState<string>('');
|
||||
|
||||
// this is to update the refresh text
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const text = onLastRefreshHandler();
|
||||
if (refreshText !== text) {
|
||||
setRefreshText(text);
|
||||
}
|
||||
}, 2000);
|
||||
return (): void => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [onLastRefreshHandler, refreshText]);
|
||||
|
||||
return (
|
||||
<RefreshTextContainer refreshButtonHidden={refreshButtonHidden}>
|
||||
<Typography>{refreshText}</Typography>
|
||||
</RefreshTextContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface RefreshTextProps {
|
||||
onLastRefreshHandler: () => string;
|
||||
refreshButtonHidden: boolean;
|
||||
}
|
||||
|
||||
export default RefreshText;
|
||||
@@ -1,143 +0,0 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
type FiveMin = '5m';
|
||||
type TenMin = '10m';
|
||||
type FifteenMin = '15m';
|
||||
type ThirtyMin = '30m';
|
||||
type OneMin = '1m';
|
||||
type SixHour = '6h';
|
||||
type OneHour = '1h';
|
||||
type FourHour = '4h';
|
||||
type ThreeHour = '3h';
|
||||
type TwelveHour = '12h';
|
||||
type OneDay = '1d';
|
||||
type ThreeDay = '3d';
|
||||
type OneWeek = '1w';
|
||||
type Custom = 'custom';
|
||||
|
||||
export type Time =
|
||||
| FiveMin
|
||||
| TenMin
|
||||
| FifteenMin
|
||||
| ThirtyMin
|
||||
| OneMin
|
||||
| FourHour
|
||||
| SixHour
|
||||
| OneHour
|
||||
| ThreeHour
|
||||
| Custom
|
||||
| OneWeek
|
||||
| OneDay
|
||||
| TwelveHour
|
||||
| ThreeDay;
|
||||
|
||||
export const Options: Option[] = [
|
||||
{ value: '5m', label: 'Last 5 min' },
|
||||
{ value: '15m', label: 'Last 15 min' },
|
||||
{ value: '30m', label: 'Last 30 min' },
|
||||
{ value: '1h', label: 'Last 1 hour' },
|
||||
{ value: '6h', label: 'Last 6 hour' },
|
||||
{ value: '1d', label: 'Last 1 day' },
|
||||
{ value: '3d', label: 'Last 3 days' },
|
||||
{ value: '1w', label: 'Last 1 week' },
|
||||
{ value: 'custom', label: 'Custom' },
|
||||
];
|
||||
|
||||
type TimeFrame = {
|
||||
'5min': string;
|
||||
'15min': string;
|
||||
'30min': string;
|
||||
'1hr': string;
|
||||
'6hr': string;
|
||||
'1day': string;
|
||||
'3days': string;
|
||||
'1week': string;
|
||||
[key: string]: string; // Index signature to allow any string as index
|
||||
};
|
||||
|
||||
export const RelativeTimeMap: TimeFrame = {
|
||||
'5min': '5m',
|
||||
'15min': '15m',
|
||||
'30min': '30m',
|
||||
'1hr': '1h',
|
||||
'6hr': '6h',
|
||||
'1day': '1d',
|
||||
'3days': '3d',
|
||||
'1week': '1w',
|
||||
};
|
||||
|
||||
export interface Option {
|
||||
value: Time;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const RelativeDurationOptions: Option[] = [
|
||||
{ value: '5m', label: 'Last 5 min' },
|
||||
{ value: '15m', label: 'Last 15 min' },
|
||||
{ value: '30m', label: 'Last 30 min' },
|
||||
{ value: '1h', label: 'Last 1 hour' },
|
||||
{ value: '6h', label: 'Last 6 hour' },
|
||||
{ value: '1d', label: 'Last 1 day' },
|
||||
{ value: '3d', label: 'Last 3 days' },
|
||||
{ value: '1w', label: 'Last 1 week' },
|
||||
];
|
||||
|
||||
export const getDefaultOption = (route: string): Time => {
|
||||
if (route === ROUTES.SERVICE_MAP) {
|
||||
return RelativeDurationOptions[2].value;
|
||||
}
|
||||
if (route === ROUTES.APPLICATION) {
|
||||
return Options[2].value;
|
||||
}
|
||||
return Options[2].value;
|
||||
};
|
||||
|
||||
export const getOptions = (routes: string): Option[] => {
|
||||
if (routes === ROUTES.SERVICE_MAP) {
|
||||
return RelativeDurationOptions;
|
||||
}
|
||||
return Options;
|
||||
};
|
||||
|
||||
export const routesToHideBreadCrumbs = [ROUTES.SUPPORT, ROUTES.ALL_DASHBOARD];
|
||||
|
||||
export const routesToSkip = [
|
||||
ROUTES.HOME,
|
||||
ROUTES.SETTINGS,
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
ROUTES.TRACE_DETAIL,
|
||||
ROUTES.ALL_CHANNELS,
|
||||
ROUTES.USAGE_EXPLORER,
|
||||
ROUTES.GET_STARTED,
|
||||
ROUTES.GET_STARTED_WITH_CLOUD,
|
||||
ROUTES.GET_STARTED_APPLICATION_MONITORING,
|
||||
ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING,
|
||||
ROUTES.GET_STARTED_LOGS_MANAGEMENT,
|
||||
ROUTES.GET_STARTED_AWS_MONITORING,
|
||||
ROUTES.GET_STARTED_AZURE_MONITORING,
|
||||
ROUTES.VERSION,
|
||||
ROUTES.ALL_DASHBOARD,
|
||||
ROUTES.ORG_SETTINGS,
|
||||
ROUTES.INGESTION_SETTINGS,
|
||||
ROUTES.ERROR_DETAIL,
|
||||
ROUTES.LOGS_PIPELINES,
|
||||
ROUTES.BILLING,
|
||||
ROUTES.SUPPORT,
|
||||
ROUTES.WORKSPACE_LOCKED,
|
||||
ROUTES.WORKSPACE_SUSPENDED,
|
||||
ROUTES.LOGS,
|
||||
ROUTES.MY_SETTINGS,
|
||||
ROUTES.LIST_LICENSES,
|
||||
];
|
||||
|
||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||
|
||||
export interface LocalStorageTimeRange {
|
||||
localstorageStartTime: string | null;
|
||||
localstorageEndTime: string | null;
|
||||
}
|
||||
|
||||
export interface TimeRange {
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}
|
||||
@@ -1,425 +0,0 @@
|
||||
import './DateTimeSelection.styles.scss';
|
||||
|
||||
import { SyncOutlined } from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import CustomTimePicker from 'components/CustomTimePicker/CustomTimePicker';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import history from 'lib/history';
|
||||
import { isObject } from 'lodash-es';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { GlobalTimeLoading, UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import AutoRefresh from '../AutoRefresh';
|
||||
import CustomDateTimeModal, { DateTimeRangeType } from '../CustomDateTimeModal';
|
||||
import { CustomTimeType, Time as TimeV2 } from '../DateTimeSelectionV2/config';
|
||||
import {
|
||||
getDefaultOption,
|
||||
getOptions,
|
||||
LocalStorageTimeRange,
|
||||
Time,
|
||||
TimeRange,
|
||||
} from './config';
|
||||
import RefreshText from './Refresh';
|
||||
import { Form, FormContainer, FormItem } from './styles';
|
||||
|
||||
function DateTimeSelection({
|
||||
location,
|
||||
updateTimeInterval,
|
||||
globalTimeLoading,
|
||||
}: Props): JSX.Element {
|
||||
const [formSelector] = Form.useForm();
|
||||
|
||||
const [hasSelectedTimeError, setHasSelectedTimeError] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
const searchStartTime = urlQuery.get('startTime');
|
||||
const searchEndTime = urlQuery.get('endTime');
|
||||
|
||||
const {
|
||||
localstorageStartTime,
|
||||
localstorageEndTime,
|
||||
} = ((): LocalStorageTimeRange => {
|
||||
const routes = getLocalStorageKey(LOCALSTORAGE.METRICS_TIME_IN_DURATION);
|
||||
|
||||
if (routes !== null) {
|
||||
const routesObject = JSON.parse(routes || '{}');
|
||||
const selectedTime = routesObject[location.pathname];
|
||||
|
||||
if (selectedTime) {
|
||||
let parsedSelectedTime: TimeRange;
|
||||
try {
|
||||
parsedSelectedTime = JSON.parse(selectedTime);
|
||||
} catch {
|
||||
parsedSelectedTime = selectedTime;
|
||||
}
|
||||
|
||||
if (isObject(parsedSelectedTime)) {
|
||||
return {
|
||||
localstorageStartTime: parsedSelectedTime.startTime,
|
||||
localstorageEndTime: parsedSelectedTime.endTime,
|
||||
};
|
||||
}
|
||||
return { localstorageStartTime: null, localstorageEndTime: null };
|
||||
}
|
||||
}
|
||||
return { localstorageStartTime: null, localstorageEndTime: null };
|
||||
})();
|
||||
|
||||
const getTime = useCallback((): [number, number] | undefined => {
|
||||
if (searchEndTime && searchStartTime) {
|
||||
const startDate = dayjs(
|
||||
new Date(parseInt(getTimeString(searchStartTime), 10)),
|
||||
);
|
||||
const endDate = dayjs(new Date(parseInt(getTimeString(searchEndTime), 10)));
|
||||
|
||||
return [startDate.toDate().getTime() || 0, endDate.toDate().getTime() || 0];
|
||||
}
|
||||
if (localstorageStartTime && localstorageEndTime) {
|
||||
const startDate = dayjs(localstorageStartTime);
|
||||
const endDate = dayjs(localstorageEndTime);
|
||||
|
||||
return [startDate.toDate().getTime() || 0, endDate.toDate().getTime() || 0];
|
||||
}
|
||||
return undefined;
|
||||
}, [
|
||||
localstorageEndTime,
|
||||
localstorageStartTime,
|
||||
searchEndTime,
|
||||
searchStartTime,
|
||||
]);
|
||||
|
||||
const [options, setOptions] = useState(getOptions(location.pathname));
|
||||
const [refreshButtonHidden, setRefreshButtonHidden] = useState<boolean>(false);
|
||||
const [customDateTimeVisible, setCustomDTPickerVisible] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
|
||||
const { stagedQuery, initQueryBuilderData } = useQueryBuilder();
|
||||
|
||||
const { maxTime, minTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const getInputLabel = (
|
||||
startTime?: Dayjs,
|
||||
endTime?: Dayjs,
|
||||
timeInterval: Time | TimeV2 | CustomTimeType = '15m',
|
||||
): string | Time => {
|
||||
if (startTime && endTime && timeInterval === 'custom') {
|
||||
const format = DATE_TIME_FORMATS.SLASH_DATETIME;
|
||||
|
||||
const startString = startTime.format(format);
|
||||
const endString = endTime.format(format);
|
||||
|
||||
return `${startString} - ${endString}`;
|
||||
}
|
||||
|
||||
return timeInterval;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTime === 'custom') {
|
||||
setRefreshButtonHidden(true);
|
||||
} else {
|
||||
setRefreshButtonHidden(false);
|
||||
}
|
||||
}, [selectedTime]);
|
||||
|
||||
const getDefaultTime = (pathName: string): Time => {
|
||||
const defaultSelectedOption = getDefaultOption(pathName);
|
||||
|
||||
const routes = getLocalStorageKey(LOCALSTORAGE.METRICS_TIME_IN_DURATION);
|
||||
|
||||
if (routes !== null) {
|
||||
const routesObject = JSON.parse(routes || '{}');
|
||||
const selectedTime = routesObject[pathName];
|
||||
|
||||
if (selectedTime) {
|
||||
let parsedSelectedTime: TimeRange;
|
||||
try {
|
||||
parsedSelectedTime = JSON.parse(selectedTime);
|
||||
} catch {
|
||||
parsedSelectedTime = selectedTime;
|
||||
}
|
||||
if (isObject(parsedSelectedTime)) {
|
||||
return 'custom';
|
||||
}
|
||||
return selectedTime;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultSelectedOption;
|
||||
};
|
||||
|
||||
const updateLocalStorageForRoutes = (value: Time | TimeV2 | string): void => {
|
||||
const preRoutes = getLocalStorageKey(LOCALSTORAGE.METRICS_TIME_IN_DURATION);
|
||||
if (preRoutes !== null) {
|
||||
const preRoutesObject = JSON.parse(preRoutes);
|
||||
|
||||
const preRoute = {
|
||||
...preRoutesObject,
|
||||
};
|
||||
preRoute[location.pathname] = value;
|
||||
|
||||
setLocalStorageKey(
|
||||
LOCALSTORAGE.METRICS_TIME_IN_DURATION,
|
||||
JSON.stringify(preRoute),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onLastRefreshHandler = useCallback(() => {
|
||||
const currentTime = dayjs();
|
||||
|
||||
const lastRefresh = dayjs(
|
||||
selectedTime === 'custom' ? minTime / 1000000 : maxTime / 1000000,
|
||||
);
|
||||
|
||||
const secondsDiff = currentTime.diff(lastRefresh, 'seconds');
|
||||
|
||||
const minutedDiff = currentTime.diff(lastRefresh, 'minutes');
|
||||
const hoursDiff = currentTime.diff(lastRefresh, 'hours');
|
||||
const daysDiff = currentTime.diff(lastRefresh, 'days');
|
||||
const monthsDiff = currentTime.diff(lastRefresh, 'months');
|
||||
|
||||
if (monthsDiff > 0) {
|
||||
return `Last refresh -${monthsDiff} months ago`;
|
||||
}
|
||||
|
||||
if (daysDiff > 0) {
|
||||
return `Last refresh - ${daysDiff} days ago`;
|
||||
}
|
||||
|
||||
if (hoursDiff > 0) {
|
||||
return `Last refresh - ${hoursDiff} hrs ago`;
|
||||
}
|
||||
|
||||
if (minutedDiff > 0) {
|
||||
return `Last refresh - ${minutedDiff} mins ago`;
|
||||
}
|
||||
|
||||
return `Last refresh - ${secondsDiff} sec ago`;
|
||||
}, [maxTime, minTime, selectedTime]);
|
||||
|
||||
const isLogsExplorerPage = useMemo(
|
||||
() => location.pathname === ROUTES.LOGS_EXPLORER,
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const onSelectHandler = (value: Time | TimeV2 | CustomTimeType): void => {
|
||||
if (value !== 'custom') {
|
||||
updateTimeInterval(value);
|
||||
updateLocalStorageForRoutes(value);
|
||||
if (refreshButtonHidden) {
|
||||
setRefreshButtonHidden(false);
|
||||
}
|
||||
} else {
|
||||
setRefreshButtonHidden(true);
|
||||
setCustomDTPickerVisible(true);
|
||||
}
|
||||
|
||||
if (!isLogsExplorerPage) {
|
||||
urlQuery.set(QueryParams.startTime, minTime.toString());
|
||||
urlQuery.set(QueryParams.endTime, maxTime.toString());
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
history.push(generatedUrl);
|
||||
}
|
||||
|
||||
if (!stagedQuery) {
|
||||
return;
|
||||
}
|
||||
initQueryBuilderData(updateStepInterval(stagedQuery));
|
||||
};
|
||||
|
||||
const onRefreshHandler = (): void => {
|
||||
onSelectHandler(selectedTime);
|
||||
onLastRefreshHandler();
|
||||
};
|
||||
|
||||
const onCustomDateHandler = (dateTimeRange: DateTimeRangeType): void => {
|
||||
if (dateTimeRange !== null) {
|
||||
const [startTimeMoment, endTimeMoment] = dateTimeRange;
|
||||
if (startTimeMoment && endTimeMoment) {
|
||||
updateTimeInterval('custom', [
|
||||
startTimeMoment?.toDate().getTime() || 0,
|
||||
endTimeMoment?.toDate().getTime() || 0,
|
||||
]);
|
||||
updateLocalStorageForRoutes(
|
||||
JSON.stringify({ startTime: startTimeMoment, endTime: endTimeMoment }),
|
||||
);
|
||||
if (!isLogsExplorerPage) {
|
||||
urlQuery.set(
|
||||
QueryParams.startTime,
|
||||
startTimeMoment?.toDate().getTime().toString(),
|
||||
);
|
||||
urlQuery.set(
|
||||
QueryParams.endTime,
|
||||
endTimeMoment?.toDate().getTime().toString(),
|
||||
);
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
history.push(generatedUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// this is triggred when we change the routes and based on that we are changing the default options
|
||||
useEffect(() => {
|
||||
const metricsTimeDuration = getLocalStorageKey(
|
||||
LOCALSTORAGE.METRICS_TIME_IN_DURATION,
|
||||
);
|
||||
|
||||
if (metricsTimeDuration === null) {
|
||||
setLocalStorageKey(
|
||||
LOCALSTORAGE.METRICS_TIME_IN_DURATION,
|
||||
JSON.stringify({}),
|
||||
);
|
||||
}
|
||||
|
||||
const currentRoute = location.pathname;
|
||||
const time = getDefaultTime(currentRoute);
|
||||
|
||||
const currentOptions = getOptions(currentRoute);
|
||||
setOptions(currentOptions);
|
||||
|
||||
const getCustomOrIntervalTime = (time: Time): Time => {
|
||||
if (searchEndTime !== null && searchStartTime !== null) {
|
||||
return 'custom';
|
||||
}
|
||||
if (
|
||||
(localstorageEndTime === null || localstorageStartTime === null) &&
|
||||
time === 'custom'
|
||||
) {
|
||||
return getDefaultOption(currentRoute);
|
||||
}
|
||||
|
||||
return time;
|
||||
};
|
||||
|
||||
const updatedTime = getCustomOrIntervalTime(time);
|
||||
|
||||
const [preStartTime = 0, preEndTime = 0] = getTime() || [];
|
||||
|
||||
setRefreshButtonHidden(updatedTime === 'custom');
|
||||
|
||||
updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
|
||||
|
||||
if (updatedTime !== 'custom') {
|
||||
const { minTime, maxTime } = GetMinMax(updatedTime);
|
||||
urlQuery.set(QueryParams.startTime, minTime.toString());
|
||||
urlQuery.set(QueryParams.endTime, maxTime.toString());
|
||||
} else {
|
||||
urlQuery.set(QueryParams.startTime, preStartTime.toString());
|
||||
urlQuery.set(QueryParams.endTime, preEndTime.toString());
|
||||
}
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
history.replace(generatedUrl);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.pathname, updateTimeInterval, globalTimeLoading]);
|
||||
|
||||
return (
|
||||
<div className="date-time-selection-container">
|
||||
<Form
|
||||
form={formSelector}
|
||||
layout="inline"
|
||||
initialValues={{ interval: selectedTime }}
|
||||
>
|
||||
<FormContainer>
|
||||
<CustomTimePicker
|
||||
open={isOpen}
|
||||
setOpen={setIsOpen}
|
||||
onSelect={(value: unknown): void => {
|
||||
onSelectHandler(value as Time);
|
||||
}}
|
||||
onError={(hasError: boolean): void => {
|
||||
setHasSelectedTimeError(hasError);
|
||||
}}
|
||||
selectedTime={selectedTime}
|
||||
onValidCustomDateChange={(dateTime): void =>
|
||||
onCustomDateHandler(dateTime.time as DateTimeRangeType)
|
||||
}
|
||||
selectedValue={getInputLabel(
|
||||
dayjs(minTime / 1000000),
|
||||
dayjs(maxTime / 1000000),
|
||||
selectedTime,
|
||||
)}
|
||||
data-testid="dropDown"
|
||||
items={options}
|
||||
minTime={minTime}
|
||||
maxTime={maxTime}
|
||||
/>
|
||||
|
||||
<FormItem hidden={refreshButtonHidden}>
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
type="primary"
|
||||
onClick={onRefreshHandler}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<AutoRefresh disabled={refreshButtonHidden} />
|
||||
</FormItem>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
|
||||
{!hasSelectedTimeError && selectedTime !== 'custom' && (
|
||||
<RefreshText
|
||||
{...{
|
||||
onLastRefreshHandler,
|
||||
}}
|
||||
refreshButtonHidden={refreshButtonHidden}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CustomDateTimeModal
|
||||
visible={customDateTimeVisible}
|
||||
onCreate={onCustomDateHandler}
|
||||
onCancel={(): void => {
|
||||
setCustomDTPickerVisible(false);
|
||||
}}
|
||||
setCustomDTPickerVisible={setCustomDTPickerVisible}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
updateTimeInterval: (
|
||||
interval: Time | TimeV2 | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => (dispatch: Dispatch<AppActions>) => void;
|
||||
globalTimeLoading: () => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
updateTimeInterval: bindActionCreators(UpdateTimeInterval, dispatch),
|
||||
globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch),
|
||||
});
|
||||
|
||||
type Props = DispatchProps & RouteComponentProps;
|
||||
|
||||
export default connect(null, mapDispatchToProps)(withRouter(DateTimeSelection));
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Form as FormComponent, Typography as TypographyComponent } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Form = styled(FormComponent)`
|
||||
&&& {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Typography = styled(TypographyComponent)`
|
||||
&&& {
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FormItem = styled(Form.Item)`
|
||||
&&& {
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
refreshButtonHidden: boolean;
|
||||
}
|
||||
|
||||
export const RefreshTextContainer = styled.div<Props>`
|
||||
visibility: ${({ refreshButtonHidden }): string =>
|
||||
refreshButtonHidden ? 'hidden' : 'visible'};
|
||||
`;
|
||||
|
||||
export const FormContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 0.1rem;
|
||||
`;
|
||||
@@ -1,54 +1,6 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
type FiveMin = '5m';
|
||||
type TenMin = '10m';
|
||||
type FifteenMin = '15m';
|
||||
type ThirtyMin = '30m';
|
||||
type FortyFiveMin = '45m';
|
||||
type OneMin = '1m';
|
||||
type ThreeHour = '3h';
|
||||
type SixHour = '6h';
|
||||
type OneHour = '1h';
|
||||
type FourHour = '4h';
|
||||
type TwelveHour = '12h';
|
||||
type OneDay = '1d';
|
||||
type ThreeDay = '3d';
|
||||
type FourDay = '4d';
|
||||
type TenDay = '10d';
|
||||
type OneWeek = '1w';
|
||||
type TwoWeek = '2w';
|
||||
type SixWeek = '6w';
|
||||
type OneMonth = '1month';
|
||||
type TwoMonths = '2months';
|
||||
type Custom = 'custom';
|
||||
|
||||
export type Time =
|
||||
| FiveMin
|
||||
| TenMin
|
||||
| FifteenMin
|
||||
| ThirtyMin
|
||||
| OneMin
|
||||
| ThreeHour
|
||||
| FourHour
|
||||
| SixHour
|
||||
| OneHour
|
||||
| Custom
|
||||
| OneWeek
|
||||
| SixWeek
|
||||
| OneDay
|
||||
| FourDay
|
||||
| ThreeDay
|
||||
| FortyFiveMin
|
||||
| TwelveHour
|
||||
| TenDay
|
||||
| TwoWeek
|
||||
| OneMonth
|
||||
| TwoMonths;
|
||||
|
||||
export type TimeUnit = 'm' | 'h' | 'd' | 'w';
|
||||
|
||||
export type CustomTimeType = `${string}${TimeUnit}`;
|
||||
import { CustomTimeType, Option, Time, TimeFrame } from './types';
|
||||
|
||||
export const Options: Option[] = [
|
||||
{ value: '5m', label: 'Last 5 minutes' },
|
||||
@@ -63,10 +15,16 @@ export const Options: Option[] = [
|
||||
{ value: 'custom', label: 'Custom Date Range' },
|
||||
];
|
||||
|
||||
export interface Option {
|
||||
value: Time;
|
||||
label: string;
|
||||
}
|
||||
export const RelativeTimeMap: TimeFrame = {
|
||||
'5min': '5m',
|
||||
'15min': '15m',
|
||||
'30min': '30m',
|
||||
'1hr': '1h',
|
||||
'6hr': '6h',
|
||||
'1day': '1d',
|
||||
'3days': '3d',
|
||||
'1week': '1w',
|
||||
};
|
||||
|
||||
export const OLD_RELATIVE_TIME_VALUES = [
|
||||
'1min',
|
||||
@@ -244,18 +202,3 @@ export const routesToSkip = [
|
||||
];
|
||||
|
||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||
|
||||
export interface LocalStorageTimeRange {
|
||||
localstorageStartTime: string | null;
|
||||
localstorageEndTime: string | null;
|
||||
}
|
||||
|
||||
export interface TimeRange {
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}
|
||||
|
||||
export enum LexicalContext {
|
||||
CUSTOM_DATE_PICKER = 'customDatePicker',
|
||||
CUSTOM_DATE_TIME_INPUT = 'customDateTimeInput',
|
||||
}
|
||||
@@ -35,19 +35,21 @@ import { v4 as uuid } from 'uuid';
|
||||
|
||||
import AutoRefresh from '../AutoRefreshV2';
|
||||
import { DateTimeRangeType } from '../CustomDateTimeModal';
|
||||
import { RelativeTimeMap } from '../DateTimeSelection/config';
|
||||
import {
|
||||
convertOldTimeToNewValidCustomTimeFormat,
|
||||
CustomTimeType,
|
||||
getDefaultOption,
|
||||
getOptions,
|
||||
LocalStorageTimeRange,
|
||||
OLD_RELATIVE_TIME_VALUES,
|
||||
Time,
|
||||
TimeRange,
|
||||
} from './config';
|
||||
RelativeTimeMap,
|
||||
} from './constants';
|
||||
import RefreshText from './Refresh';
|
||||
import { Form, FormContainer, FormItem } from './styles';
|
||||
import {
|
||||
CustomTimeType,
|
||||
LocalStorageTimeRange,
|
||||
Time,
|
||||
TimeRange,
|
||||
} from './types';
|
||||
|
||||
function DateTimeSelection({
|
||||
showAutoRefresh,
|
||||
|
||||
80
frontend/src/container/TopNav/DateTimeSelectionV2/types.ts
Normal file
80
frontend/src/container/TopNav/DateTimeSelectionV2/types.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
type FiveMin = '5m';
|
||||
type TenMin = '10m';
|
||||
type FifteenMin = '15m';
|
||||
type ThirtyMin = '30m';
|
||||
type FortyFiveMin = '45m';
|
||||
type OneMin = '1m';
|
||||
type ThreeHour = '3h';
|
||||
type SixHour = '6h';
|
||||
type OneHour = '1h';
|
||||
type FourHour = '4h';
|
||||
type TwelveHour = '12h';
|
||||
type OneDay = '1d';
|
||||
type ThreeDay = '3d';
|
||||
type FourDay = '4d';
|
||||
type TenDay = '10d';
|
||||
type OneWeek = '1w';
|
||||
type TwoWeek = '2w';
|
||||
type SixWeek = '6w';
|
||||
type OneMonth = '1month';
|
||||
type TwoMonths = '2months';
|
||||
type Custom = 'custom';
|
||||
|
||||
export type Time =
|
||||
| FiveMin
|
||||
| TenMin
|
||||
| FifteenMin
|
||||
| ThirtyMin
|
||||
| OneMin
|
||||
| ThreeHour
|
||||
| FourHour
|
||||
| SixHour
|
||||
| OneHour
|
||||
| Custom
|
||||
| OneWeek
|
||||
| SixWeek
|
||||
| OneDay
|
||||
| FourDay
|
||||
| ThreeDay
|
||||
| FortyFiveMin
|
||||
| TwelveHour
|
||||
| TenDay
|
||||
| TwoWeek
|
||||
| OneMonth
|
||||
| TwoMonths;
|
||||
|
||||
export type TimeUnit = 'm' | 'h' | 'd' | 'w';
|
||||
|
||||
export type CustomTimeType = `${string}${TimeUnit}`;
|
||||
|
||||
export interface Option {
|
||||
value: Time;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export type TimeFrame = {
|
||||
'5min': string;
|
||||
'15min': string;
|
||||
'30min': string;
|
||||
'1hr': string;
|
||||
'6hr': string;
|
||||
'1day': string;
|
||||
'3days': string;
|
||||
'1week': string;
|
||||
[key: string]: string; // Index signature to allow any string as index
|
||||
};
|
||||
|
||||
export interface LocalStorageTimeRange {
|
||||
localstorageStartTime: string | null;
|
||||
localstorageEndTime: string | null;
|
||||
}
|
||||
|
||||
export interface TimeRange {
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}
|
||||
|
||||
export enum LexicalContext {
|
||||
CUSTOM_DATE_PICKER = 'customDatePicker',
|
||||
CUSTOM_DATE_TIME_INPUT = 'customDateTimeInput',
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { matchPath, useHistory } from 'react-router-dom';
|
||||
|
||||
import NewExplorerCTA from '../NewExplorerCTA';
|
||||
import DateTimeSelector from './DateTimeSelectionV2';
|
||||
import { routesToDisable, routesToSkip } from './DateTimeSelectionV2/config';
|
||||
import { routesToDisable, routesToSkip } from './DateTimeSelectionV2/constants';
|
||||
|
||||
function TopNav(): JSX.Element | null {
|
||||
const { location } = useHistory();
|
||||
|
||||
@@ -12,7 +12,7 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
||||
import NoLogs from 'container/NoLogs/NoLogs';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import TraceExplorerControls from 'container/TracesExplorer/Controls';
|
||||
import { getListViewQuery } from 'container/TracesExplorer/explorerUtils';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
|
||||
183
frontend/src/hooks/dashboard/utils.test.ts
Normal file
183
frontend/src/hooks/dashboard/utils.test.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import {
|
||||
initialClickHouseData,
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryPromQLData,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { IDashboardVariable, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { createDynamicVariableToWidgetsMap } from './utils';
|
||||
|
||||
const createMockDynamicVariable = (
|
||||
overrides: Partial<IDashboardVariable> = {},
|
||||
): IDashboardVariable => ({
|
||||
id: 'var-1',
|
||||
name: 'testVar',
|
||||
description: '',
|
||||
type: 'DYNAMIC',
|
||||
sort: 'DISABLED',
|
||||
multiSelect: false,
|
||||
showALLOption: false,
|
||||
dynamicVariablesAttribute: 'service.name',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createBaseWidget = (id: string, query: Query): Widgets => ({
|
||||
id,
|
||||
title: 'Test Widget',
|
||||
description: '',
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
opacity: '1',
|
||||
nullZeroValues: '',
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
softMin: null,
|
||||
softMax: null,
|
||||
selectedLogFields: null,
|
||||
selectedTracesFields: null,
|
||||
query,
|
||||
});
|
||||
|
||||
const createMockPromQLWidget = (
|
||||
id: string,
|
||||
queries: {
|
||||
query: string;
|
||||
name?: string;
|
||||
legend?: string;
|
||||
disabled?: boolean;
|
||||
}[],
|
||||
): Widgets => {
|
||||
const promqlQueries = queries.map((q) => ({
|
||||
...initialQueryPromQLData,
|
||||
query: q.query,
|
||||
name: q.name || 'A',
|
||||
legend: q.legend || '',
|
||||
disabled: q.disabled ?? false,
|
||||
}));
|
||||
|
||||
const query: Query = {
|
||||
queryType: EQueryType.PROM,
|
||||
promql: promqlQueries,
|
||||
builder: {
|
||||
queryData: [],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [],
|
||||
id: 'query-1',
|
||||
};
|
||||
|
||||
return createBaseWidget(id, query);
|
||||
};
|
||||
|
||||
const createMockClickHouseWidget = (
|
||||
id: string,
|
||||
queries: {
|
||||
query: string;
|
||||
name?: string;
|
||||
legend?: string;
|
||||
disabled?: boolean;
|
||||
}[],
|
||||
): Widgets => {
|
||||
const clickhouseQueries = queries.map((q) => ({
|
||||
...initialClickHouseData,
|
||||
query: q.query,
|
||||
name: q.name || 'A',
|
||||
legend: q.legend || '',
|
||||
disabled: q.disabled ?? false,
|
||||
}));
|
||||
|
||||
const query: Query = {
|
||||
queryType: EQueryType.CLICKHOUSE,
|
||||
promql: [],
|
||||
builder: {
|
||||
queryData: [],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: clickhouseQueries,
|
||||
id: 'query-1',
|
||||
};
|
||||
|
||||
return createBaseWidget(id, query);
|
||||
};
|
||||
|
||||
const createMockQueryBuilderWidget = (
|
||||
id: string,
|
||||
filters: { key: string; value: string | string[]; op?: string }[],
|
||||
): Widgets => {
|
||||
const queryData = {
|
||||
...initialQueryBuilderFormValuesMap[DataSource.LOGS],
|
||||
queryName: 'A',
|
||||
filters: {
|
||||
items: filters.map((f, index) => ({
|
||||
id: `filter-${index}`,
|
||||
key: { key: f.key, dataType: DataTypes.String, type: '', id: f.key },
|
||||
op: f.op || '=',
|
||||
value: f.value,
|
||||
})),
|
||||
op: 'AND',
|
||||
},
|
||||
};
|
||||
|
||||
const query: Query = {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
promql: [],
|
||||
builder: {
|
||||
queryData: [queryData],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [],
|
||||
id: 'query-1',
|
||||
};
|
||||
|
||||
return createBaseWidget(id, query);
|
||||
};
|
||||
|
||||
describe('createDynamicVariableToWidgetsMap', () => {
|
||||
it('should handle widgets with different query types', () => {
|
||||
const dynamicVariables = [
|
||||
createMockDynamicVariable({
|
||||
id: 'var-1',
|
||||
name: 'service.name123',
|
||||
dynamicVariablesAttribute: 'service.name',
|
||||
}),
|
||||
];
|
||||
|
||||
const widgets = [
|
||||
createMockPromQLWidget('widget-promql-pass', [
|
||||
{ query: 'up{service="$service.name123"}' },
|
||||
]),
|
||||
createMockPromQLWidget('widget-promql-fail', [
|
||||
{ query: 'up{service="$service.name"}' },
|
||||
]),
|
||||
createMockClickHouseWidget('widget-clickhouse-pass', [
|
||||
{ query: "SELECT * FROM logs WHERE service_name = '$service.name123'" },
|
||||
]),
|
||||
createMockClickHouseWidget('widget-clickhouse-fail', [
|
||||
{ query: "SELECT * FROM logs WHERE service_name = '$service.name'" },
|
||||
]),
|
||||
createMockQueryBuilderWidget('widget-builder-pass', [
|
||||
{ key: 'service.name', value: '$service.name123' },
|
||||
]),
|
||||
createMockQueryBuilderWidget('widget-builder-fail', [
|
||||
{ key: 'service.name', value: '$service.name' },
|
||||
]),
|
||||
];
|
||||
|
||||
const result = createDynamicVariableToWidgetsMap(dynamicVariables, widgets);
|
||||
|
||||
expect(result['var-1']).toContain('widget-promql-pass');
|
||||
expect(result['var-1']).toContain('widget-clickhouse-pass');
|
||||
expect(result['var-1']).toContain('widget-builder-pass');
|
||||
|
||||
expect(result['var-1']).not.toContain('widget-promql-fail');
|
||||
expect(result['var-1']).not.toContain('widget-clickhouse-fail');
|
||||
expect(result['var-1']).not.toContain('widget-builder-fail');
|
||||
});
|
||||
});
|
||||
@@ -104,10 +104,9 @@ export const createDynamicVariableToWidgetsMap = (
|
||||
// Check each widget for usage of dynamic variables
|
||||
if (Array.isArray(widgets)) {
|
||||
widgets.forEach((widget) => {
|
||||
if (
|
||||
widget.query?.builder?.queryData &&
|
||||
widget.query?.queryType === EQueryType.QUERY_BUILDER
|
||||
) {
|
||||
if (widget.query?.queryType === EQueryType.QUERY_BUILDER) {
|
||||
if (!Array.isArray(widget.query.builder.queryData)) return;
|
||||
|
||||
widget.query.builder.queryData.forEach((queryData: IBuilderQuery) => {
|
||||
// Check filter items for dynamic variables
|
||||
queryData.filters?.items?.forEach((filter: TagFilterItem) => {
|
||||
@@ -139,6 +138,34 @@ export const createDynamicVariableToWidgetsMap = (
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (widget.query?.queryType === EQueryType.PROM) {
|
||||
if (!Array.isArray(widget.query.promql)) return;
|
||||
|
||||
widget.query.promql.forEach((promqlQuery) => {
|
||||
dynamicVariables.forEach((variable) => {
|
||||
if (
|
||||
variable.dynamicVariablesAttribute &&
|
||||
promqlQuery.query?.includes(`$${variable.name}`) &&
|
||||
!dynamicVariableToWidgetsMap[variable.id].includes(widget.id)
|
||||
) {
|
||||
dynamicVariableToWidgetsMap[variable.id].push(widget.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (widget.query?.queryType === EQueryType.CLICKHOUSE) {
|
||||
if (!Array.isArray(widget.query.clickhouse_sql)) return;
|
||||
|
||||
widget.query.clickhouse_sql.forEach((clickhouseQuery) => {
|
||||
dynamicVariables.forEach((variable) => {
|
||||
if (
|
||||
variable.dynamicVariablesAttribute &&
|
||||
clickhouseQuery.query?.includes(`$${variable.name}`) &&
|
||||
!dynamicVariableToWidgetsMap[variable.id].includes(widget.id)
|
||||
) {
|
||||
dynamicVariableToWidgetsMap[variable.id].push(widget.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import getService from 'api/metrics/getService';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time as TimeV2,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import {
|
||||
QueryKey,
|
||||
useQuery,
|
||||
@@ -30,7 +29,7 @@ export const useQueryService = ({
|
||||
interface UseQueryServiceProps {
|
||||
minTime: number;
|
||||
maxTime: number;
|
||||
selectedTime: Time | TimeV2 | CustomTimeType;
|
||||
selectedTime: Time | CustomTimeType;
|
||||
selectedTags: Tags[];
|
||||
options?: UseQueryOptions<PayloadProps, AxiosError, PayloadProps, QueryKey>;
|
||||
}
|
||||
|
||||
132
frontend/src/lib/__tests__/getConvertedValue.test.ts
Normal file
132
frontend/src/lib/__tests__/getConvertedValue.test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { convertValue, getFormattedUnit } from 'lib/getConvertedValue';
|
||||
|
||||
describe('getFormattedUnit', () => {
|
||||
it('should return the grafana unit for universal unit if it exists', () => {
|
||||
const formattedUnit = getFormattedUnit(UniversalYAxisUnit.KILOBYTES);
|
||||
expect(formattedUnit).toBe('deckbytes');
|
||||
});
|
||||
|
||||
it('should return the unit directly if it is not a universal unit', () => {
|
||||
const formattedUnit = getFormattedUnit('{reason}');
|
||||
expect(formattedUnit).toBe('{reason}');
|
||||
});
|
||||
|
||||
it('should return the universal unit directly if it does not have a grafana equivalent', () => {
|
||||
const formattedUnit = getFormattedUnit(UniversalYAxisUnit.EXABYTES);
|
||||
expect(formattedUnit).toBe(UniversalYAxisUnit.EXABYTES);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertValue', () => {
|
||||
describe('data', () => {
|
||||
it('should convert bytes (IEC) to kilobytes', () => {
|
||||
expect(
|
||||
convertValue(
|
||||
1000,
|
||||
UniversalYAxisUnit.BYTES_IEC,
|
||||
UniversalYAxisUnit.KILOBYTES,
|
||||
),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should convert bytes (SI) to kilobytes', () => {
|
||||
expect(
|
||||
convertValue(1000, UniversalYAxisUnit.BYTES, UniversalYAxisUnit.KILOBYTES),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should convert kilobytes to bytes', () => {
|
||||
expect(
|
||||
convertValue(1, UniversalYAxisUnit.KILOBYTES, UniversalYAxisUnit.BYTES),
|
||||
).toBe(1000);
|
||||
});
|
||||
|
||||
it('should convert megabytes to kilobytes', () => {
|
||||
expect(convertValue(1, 'mbytes', 'kbytes')).toBe(1024);
|
||||
});
|
||||
|
||||
it('should convert gigabytes to megabytes', () => {
|
||||
expect(convertValue(1, 'gbytes', 'mbytes')).toBe(1024);
|
||||
});
|
||||
|
||||
it('should convert kilobytes to megabytes', () => {
|
||||
expect(convertValue(1024, 'kbytes', 'mbytes')).toBe(1);
|
||||
});
|
||||
|
||||
it('should convert bits to gigabytes', () => {
|
||||
// 12 GB = 103079215104 bits
|
||||
expect(convertValue(103079215104, 'bits', 'gbytes')).toBe(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('time', () => {
|
||||
it('should convert milliseconds to seconds', () => {
|
||||
expect(convertValue(1000, 'ms', 's')).toBe(1);
|
||||
});
|
||||
|
||||
it('should convert seconds to milliseconds', () => {
|
||||
expect(convertValue(1, 's', 'ms')).toBe(1000);
|
||||
});
|
||||
|
||||
it('should convert nanoseconds to milliseconds', () => {
|
||||
expect(convertValue(1000000, 'ns', 'ms')).toBe(1);
|
||||
});
|
||||
|
||||
it('should convert seconds to minutes', () => {
|
||||
expect(convertValue(60, 's', 'm')).toBe(1);
|
||||
});
|
||||
|
||||
it('should convert minutes to hours', () => {
|
||||
expect(convertValue(60, 'm', 'h')).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data rate', () => {
|
||||
it('should convert bytes/sec to kibibytes/sec', () => {
|
||||
expect(convertValue(1024, 'binBps', 'KiBs')).toBe(1);
|
||||
});
|
||||
|
||||
it('should convert kibibytes/sec to bytes/sec', () => {
|
||||
expect(convertValue(1, 'KiBs', 'binBps')).toBe(1024);
|
||||
});
|
||||
});
|
||||
|
||||
describe('throughput', () => {
|
||||
it('should convert counts per second to counts per minute', () => {
|
||||
expect(convertValue(1, 'cps', 'cpm')).toBe(1 / 60);
|
||||
});
|
||||
|
||||
it('should convert operations per second to operations per minute', () => {
|
||||
expect(convertValue(1, 'ops', 'opm')).toBe(1 / 60);
|
||||
});
|
||||
|
||||
it('should convert counts per minute to counts per second', () => {
|
||||
expect(convertValue(1, 'cpm', 'cps')).toBe(60);
|
||||
});
|
||||
|
||||
it('should convert operations per minute to operations per second', () => {
|
||||
expect(convertValue(1, 'opm', 'ops')).toBe(60);
|
||||
});
|
||||
});
|
||||
|
||||
describe('percent', () => {
|
||||
it('should convert percentunit to percent', () => {
|
||||
expect(convertValue(0.5, 'percentunit', 'percent')).toBe(50);
|
||||
});
|
||||
|
||||
it('should convert percent to percentunit', () => {
|
||||
expect(convertValue(50, 'percent', 'percentunit')).toBe(0.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid values', () => {
|
||||
it('should return null when currentUnit is invalid', () => {
|
||||
expect(convertValue(100, 'invalidUnit', 'bytes')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when targetUnit is invalid', () => {
|
||||
expect(convertValue(100, 'bytes', 'invalidUnit')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user