Compare commits
13 Commits
feat/enabl
...
feat/add-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daa73b9d19 | ||
|
|
fb671872a9 | ||
|
|
d05d394f57 | ||
|
|
b4e5085a5a | ||
|
|
88f7502a15 | ||
|
|
b0442761ac | ||
|
|
d539ca9bab | ||
|
|
c8194e9abb | ||
|
|
6814c236b2 | ||
|
|
c919102fee | ||
|
|
f1371f965e | ||
|
|
9fd4d3eeb8 | ||
|
|
e27fd996c3 |
7
.github/workflows/build-community.yaml
vendored
7
.github/workflows/build-community.yaml
vendored
@@ -3,8 +3,8 @@ name: build-community
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+'
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -69,14 +69,13 @@ jobs:
|
||||
GO_BUILD_CONTEXT: ./cmd/community
|
||||
GO_BUILD_FLAGS: >-
|
||||
-tags timetzdata
|
||||
-ldflags='-linkmode external -extldflags \"-static\" -s -w
|
||||
-ldflags='-s -w
|
||||
-X github.com/SigNoz/signoz/pkg/version.version=${{ needs.prepare.outputs.version }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.variant=community
|
||||
-X github.com/SigNoz/signoz/pkg/version.hash=${{ needs.prepare.outputs.hash }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.time=${{ needs.prepare.outputs.time }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}
|
||||
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
|
||||
GO_CGO_ENABLED: 1
|
||||
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
|
||||
DOCKER_DOCKERFILE_PATH: ./cmd/community/Dockerfile.multi-arch
|
||||
DOCKER_MANIFEST: true
|
||||
|
||||
5
.github/workflows/build-enterprise.yaml
vendored
5
.github/workflows/build-enterprise.yaml
vendored
@@ -84,7 +84,7 @@ jobs:
|
||||
JS_INPUT_ARTIFACT_CACHE_KEY: enterprise-dotenv-${{ github.sha }}
|
||||
JS_INPUT_ARTIFACT_PATH: frontend/.env
|
||||
JS_OUTPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
|
||||
JS_OUTPUT_ARTIFACT_PATH: frontend/build
|
||||
JS_OUTPUT_ARTIFACT_PATH: frontend/build
|
||||
DOCKER_BUILD: false
|
||||
DOCKER_MANIFEST: false
|
||||
go-build:
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
GO_BUILD_CONTEXT: ./cmd/enterprise
|
||||
GO_BUILD_FLAGS: >-
|
||||
-tags timetzdata
|
||||
-ldflags='-linkmode external -extldflags \"-static\" -s -w
|
||||
-ldflags='-s -w
|
||||
-X github.com/SigNoz/signoz/pkg/version.version=${{ needs.prepare.outputs.version }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.variant=enterprise
|
||||
-X github.com/SigNoz/signoz/pkg/version.hash=${{ needs.prepare.outputs.hash }}
|
||||
@@ -110,7 +110,6 @@ jobs:
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
|
||||
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
|
||||
GO_CGO_ENABLED: 1
|
||||
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
|
||||
DOCKER_DOCKERFILE_PATH: ./cmd/enterprise/Dockerfile.multi-arch
|
||||
DOCKER_MANIFEST: true
|
||||
|
||||
5
.github/workflows/build-staging.yaml
vendored
5
.github/workflows/build-staging.yaml
vendored
@@ -98,7 +98,7 @@ jobs:
|
||||
GO_BUILD_CONTEXT: ./cmd/enterprise
|
||||
GO_BUILD_FLAGS: >-
|
||||
-tags timetzdata
|
||||
-ldflags='-linkmode external -extldflags \"-static\" -s -w
|
||||
-ldflags='-s -w
|
||||
-X github.com/SigNoz/signoz/pkg/version.version=${{ needs.prepare.outputs.version }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.variant=enterprise
|
||||
-X github.com/SigNoz/signoz/pkg/version.hash=${{ needs.prepare.outputs.hash }}
|
||||
@@ -109,7 +109,6 @@ jobs:
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.staging.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1
|
||||
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
|
||||
GO_CGO_ENABLED: 1
|
||||
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
|
||||
DOCKER_DOCKERFILE_PATH: ./cmd/enterprise/Dockerfile.multi-arch
|
||||
DOCKER_MANIFEST: true
|
||||
@@ -125,4 +124,4 @@ jobs:
|
||||
GITHUB_SILENT: true
|
||||
GITHUB_REPOSITORY_NAME: charts-saas-v3-staging
|
||||
GITHUB_EVENT_NAME: releaser
|
||||
GITHUB_EVENT_PAYLOAD: "{\"deployment\": \"${{ needs.prepare.outputs.deployment }}\", \"signoz_version\": \"${{ needs.prepare.outputs.version }}\"}"
|
||||
GITHUB_EVENT_PAYLOAD: '{"deployment": "${{ needs.prepare.outputs.deployment }}", "signoz_version": "${{ needs.prepare.outputs.version }}"}'
|
||||
|
||||
12
Makefile
12
Makefile
@@ -114,9 +114,9 @@ $(GO_BUILD_ARCHS_COMMUNITY): go-build-community-%: $(TARGET_DIR)
|
||||
@mkdir -p $(TARGET_DIR)/$(OS)-$*
|
||||
@echo ">> building binary $(TARGET_DIR)/$(OS)-$*/$(NAME)-community"
|
||||
@if [ $* = "arm64" ]; then \
|
||||
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_COMMUNITY) -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME)-community -ldflags "-linkmode external -extldflags '-static' -s -w $(GO_BUILD_LDFLAGS_COMMUNITY)"; \
|
||||
GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_COMMUNITY) -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME)-community -ldflags "-s -w $(GO_BUILD_LDFLAGS_COMMUNITY)"; \
|
||||
else \
|
||||
CGO_ENABLED=1 GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_COMMUNITY) -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME)-community -ldflags "-linkmode external -extldflags '-static' -s -w $(GO_BUILD_LDFLAGS_COMMUNITY)"; \
|
||||
GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_COMMUNITY) -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME)-community -ldflags "-s -w $(GO_BUILD_LDFLAGS_COMMUNITY)"; \
|
||||
fi
|
||||
|
||||
|
||||
@@ -127,9 +127,9 @@ $(GO_BUILD_ARCHS_ENTERPRISE): go-build-enterprise-%: $(TARGET_DIR)
|
||||
@mkdir -p $(TARGET_DIR)/$(OS)-$*
|
||||
@echo ">> building binary $(TARGET_DIR)/$(OS)-$*/$(NAME)"
|
||||
@if [ $* = "arm64" ]; then \
|
||||
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_ENTERPRISE) -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME) -ldflags "-linkmode external -extldflags '-static' -s -w $(GO_BUILD_LDFLAGS_ENTERPRISE)"; \
|
||||
GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_ENTERPRISE) -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME) -ldflags "-s -w $(GO_BUILD_LDFLAGS_ENTERPRISE)"; \
|
||||
else \
|
||||
CGO_ENABLED=1 GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_ENTERPRISE) -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME) -ldflags "-linkmode external -extldflags '-static' -s -w $(GO_BUILD_LDFLAGS_ENTERPRISE)"; \
|
||||
GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_ENTERPRISE) -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME) -ldflags "-s -w $(GO_BUILD_LDFLAGS_ENTERPRISE)"; \
|
||||
fi
|
||||
|
||||
.PHONY: go-build-enterprise-race $(GO_BUILD_ARCHS_ENTERPRISE_RACE)
|
||||
@@ -139,9 +139,9 @@ $(GO_BUILD_ARCHS_ENTERPRISE_RACE): go-build-enterprise-race-%: $(TARGET_DIR)
|
||||
@mkdir -p $(TARGET_DIR)/$(OS)-$*
|
||||
@echo ">> building binary $(TARGET_DIR)/$(OS)-$*/$(NAME)"
|
||||
@if [ $* = "arm64" ]; then \
|
||||
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_ENTERPRISE) -race -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME) -ldflags "-linkmode external -extldflags '-static' -s -w $(GO_BUILD_LDFLAGS_ENTERPRISE)"; \
|
||||
GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_ENTERPRISE) -race -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME) -ldflags "-s -w $(GO_BUILD_LDFLAGS_ENTERPRISE)"; \
|
||||
else \
|
||||
CGO_ENABLED=1 GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_ENTERPRISE) -race -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME) -ldflags "-linkmode external -extldflags '-static' -s -w $(GO_BUILD_LDFLAGS_ENTERPRISE)"; \
|
||||
GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_ENTERPRISE) -race -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME) -ldflags "-s -w $(GO_BUILD_LDFLAGS_ENTERPRISE)"; \
|
||||
fi
|
||||
|
||||
##############################################################
|
||||
|
||||
@@ -12,12 +12,6 @@ builds:
|
||||
- id: signoz
|
||||
binary: bin/signoz
|
||||
main: ./cmd/community
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- >-
|
||||
{{- if eq .Os "linux" }}
|
||||
{{- if eq .Arch "arm64" }}CC=aarch64-linux-gnu-gcc{{- end }}
|
||||
{{- end }}
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
@@ -36,8 +30,6 @@ builds:
|
||||
- -X github.com/SigNoz/signoz/pkg/version.time={{ .CommitTimestamp }}
|
||||
- -X github.com/SigNoz/signoz/pkg/version.branch={{ .Branch }}
|
||||
- -X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr
|
||||
- >-
|
||||
{{- if eq .Os "linux" }}-linkmode external -extldflags '-static'{{- end }}
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
tags:
|
||||
- timetzdata
|
||||
|
||||
@@ -12,12 +12,6 @@ builds:
|
||||
- id: signoz
|
||||
binary: bin/signoz
|
||||
main: ./cmd/enterprise
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- >-
|
||||
{{- if eq .Os "linux" }}
|
||||
{{- if eq .Arch "arm64" }}CC=aarch64-linux-gnu-gcc{{- end }}
|
||||
{{- end }}
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
@@ -40,8 +34,6 @@ builds:
|
||||
- -X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
|
||||
- -X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
|
||||
- -X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr
|
||||
- >-
|
||||
{{- if eq .Os "linux" }}-linkmode external -extldflags '-static'{{- end }}
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
tags:
|
||||
- timetzdata
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
##################### SigNoz Configuration Example #####################
|
||||
#
|
||||
#
|
||||
# Do not modify this file
|
||||
#
|
||||
|
||||
@@ -58,7 +58,7 @@ cache:
|
||||
# The port on which the Redis server is running. Default is usually 6379.
|
||||
port: 6379
|
||||
# The password for authenticating with the Redis server, if required.
|
||||
password:
|
||||
password:
|
||||
# The Redis database number to use
|
||||
db: 0
|
||||
|
||||
@@ -71,6 +71,10 @@ sqlstore:
|
||||
sqlite:
|
||||
# The path to the SQLite database file.
|
||||
path: /var/lib/signoz/signoz.db
|
||||
# Mode is the mode to use for the sqlite database.
|
||||
mode: delete
|
||||
# BusyTimeout is the timeout for the sqlite database to wait for a lock.
|
||||
busy_timeout: 10s
|
||||
|
||||
##################### APIServer #####################
|
||||
apiserver:
|
||||
@@ -238,7 +242,6 @@ statsreporter:
|
||||
# Whether to collect identities and traits (emails).
|
||||
identities: true
|
||||
|
||||
|
||||
##################### Gateway (License only) #####################
|
||||
gateway:
|
||||
# The URL of the gateway's api.
|
||||
|
||||
@@ -176,7 +176,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.97.0
|
||||
image: signoz/signoz:v0.98.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.97.0
|
||||
image: signoz/signoz:v0.98.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.97.0}
|
||||
image: signoz/signoz:${VERSION:-v0.98.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.97.0}
|
||||
image: signoz/signoz:${VERSION:-v0.98.0}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
@@ -13,8 +13,6 @@ Before diving in, make sure you have these tools installed:
|
||||
- Download from [go.dev/dl](https://go.dev/dl/)
|
||||
- Check [go.mod](../../go.mod#L3) for the minimum version
|
||||
|
||||
- **GCC** - Required for CGO dependencies
|
||||
- Download from [gcc.gnu.org](https://gcc.gnu.org/)
|
||||
|
||||
- **Node** - Powers our frontend
|
||||
- Download from [nodejs.org](https://nodejs.org)
|
||||
|
||||
@@ -2,6 +2,7 @@ package postgressqlschema
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
@@ -47,50 +48,45 @@ func (provider *provider) Operator() sqlschema.SQLOperator {
|
||||
}
|
||||
|
||||
func (provider *provider) GetTable(ctx context.Context, tableName sqlschema.TableName) (*sqlschema.Table, []*sqlschema.UniqueConstraint, error) {
|
||||
rows, err := provider.
|
||||
columns := []struct {
|
||||
ColumnName string `bun:"column_name"`
|
||||
Nullable bool `bun:"nullable"`
|
||||
SQLDataType string `bun:"udt_name"`
|
||||
DefaultVal *string `bun:"column_default"`
|
||||
}{}
|
||||
|
||||
err := provider.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
QueryContext(ctx, `
|
||||
NewRaw(`
|
||||
SELECT
|
||||
c.column_name,
|
||||
c.is_nullable = 'YES',
|
||||
c.is_nullable = 'YES' as nullable,
|
||||
c.udt_name,
|
||||
c.column_default
|
||||
FROM
|
||||
information_schema.columns AS c
|
||||
WHERE
|
||||
c.table_name = ?`, string(tableName))
|
||||
c.table_name = ?`, string(tableName)).
|
||||
Scan(ctx, &columns)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(columns) == 0 {
|
||||
return nil, nil, sql.ErrNoRows
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
columns := make([]*sqlschema.Column, 0)
|
||||
for rows.Next() {
|
||||
var (
|
||||
name string
|
||||
sqlDataType string
|
||||
nullable bool
|
||||
defaultVal *string
|
||||
)
|
||||
if err := rows.Scan(&name, &nullable, &sqlDataType, &defaultVal); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sqlschemaColumns := make([]*sqlschema.Column, 0)
|
||||
for _, column := range columns {
|
||||
columnDefault := ""
|
||||
if defaultVal != nil {
|
||||
columnDefault = *defaultVal
|
||||
if column.DefaultVal != nil {
|
||||
columnDefault = *column.DefaultVal
|
||||
}
|
||||
|
||||
columns = append(columns, &sqlschema.Column{
|
||||
Name: sqlschema.ColumnName(name),
|
||||
Nullable: nullable,
|
||||
DataType: provider.fmter.DataTypeOf(sqlDataType),
|
||||
sqlschemaColumns = append(sqlschemaColumns, &sqlschema.Column{
|
||||
Name: sqlschema.ColumnName(column.ColumnName),
|
||||
Nullable: column.Nullable,
|
||||
DataType: provider.fmter.DataTypeOf(column.SQLDataType),
|
||||
Default: columnDefault,
|
||||
})
|
||||
}
|
||||
@@ -208,7 +204,7 @@ WHERE
|
||||
|
||||
return &sqlschema.Table{
|
||||
Name: tableName,
|
||||
Columns: columns,
|
||||
Columns: sqlschemaColumns,
|
||||
PrimaryKeyConstraint: primaryKeyConstraint,
|
||||
ForeignKeyConstraints: foreignKeyConstraints,
|
||||
}, uniqueConstraints, nil
|
||||
|
||||
@@ -279,6 +279,7 @@
|
||||
"prismjs": "1.30.0",
|
||||
"got": "11.8.5",
|
||||
"form-data": "4.0.4",
|
||||
"brace-expansion": "^2.0.2"
|
||||
"brace-expansion": "^2.0.2",
|
||||
"on-headers": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,6 @@ import UPlot from 'uplot';
|
||||
|
||||
import { dataMatch, optionsUpdateState } from './utils';
|
||||
|
||||
// Extended uPlot interface with custom properties
|
||||
interface ExtendedUPlot extends uPlot {
|
||||
_legendScrollCleanup?: () => void;
|
||||
}
|
||||
|
||||
export interface UplotProps {
|
||||
options: uPlot.Options;
|
||||
data: uPlot.AlignedData;
|
||||
@@ -71,12 +66,6 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
|
||||
|
||||
const destroy = useCallback((chart: uPlot | null) => {
|
||||
if (chart) {
|
||||
// Clean up legend scroll event listener
|
||||
const extendedChart = chart as ExtendedUPlot;
|
||||
if (extendedChart._legendScrollCleanup) {
|
||||
extendedChart._legendScrollCleanup();
|
||||
}
|
||||
|
||||
onDeleteRef.current?.(chart);
|
||||
chart.destroy();
|
||||
chartRef.current = null;
|
||||
|
||||
@@ -28,7 +28,15 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
</Typography.Paragraph>
|
||||
</section>
|
||||
|
||||
<Form.Item label="Domain" name="name" className="field">
|
||||
<Form.Item
|
||||
label="Domain"
|
||||
name="name"
|
||||
className="field"
|
||||
tooltip={{
|
||||
title:
|
||||
'The email domain for users who should use SSO (e.g., `example.com` for users with `@example.com` emails)',
|
||||
}}
|
||||
>
|
||||
<Input disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
|
||||
|
||||
@@ -16,7 +16,14 @@ function ConfigureOIDCAuthnProvider({
|
||||
</Typography.Text>
|
||||
</section>
|
||||
|
||||
<Form.Item label="Domain" name="name">
|
||||
<Form.Item
|
||||
label="Domain"
|
||||
name="name"
|
||||
tooltip={{
|
||||
title:
|
||||
'The email domain for users who should use SSO (e.g., `example.com` for users with `@example.com` emails)',
|
||||
}}
|
||||
>
|
||||
<Input disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
|
||||
|
||||
@@ -16,7 +16,14 @@ function ConfigureSAMLAuthnProvider({
|
||||
</Typography.Text>
|
||||
</section>
|
||||
|
||||
<Form.Item label="Domain" name="name">
|
||||
<Form.Item
|
||||
label="Domain"
|
||||
name="name"
|
||||
tooltip={{
|
||||
title:
|
||||
'The email domain for users who should use SSO (e.g., `example.com` for users with `@example.com` emails)',
|
||||
}}
|
||||
>
|
||||
<Input disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -24,7 +31,7 @@ function ConfigureSAMLAuthnProvider({
|
||||
label="SAML ACS URL"
|
||||
name={['samlConfig', 'samlIdp']}
|
||||
tooltip={{
|
||||
title: `The entityID of the SAML identity provider. It can typically be found in the EntityID attribute of the EntityDescriptor element in the SAML metadata of the identity provider. Example: <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="{samlEntity}">`,
|
||||
title: `The SSO endpoint of the SAML identity provider. It can typically be found in the SingleSignOnService element in the SAML metadata of the identity provider. Example: <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="{samlIdp}"/>`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
@@ -34,7 +41,7 @@ function ConfigureSAMLAuthnProvider({
|
||||
label="SAML Entity ID"
|
||||
name={['samlConfig', 'samlEntity']}
|
||||
tooltip={{
|
||||
title: `The SSO endpoint of the SAML identity provider. It can typically be found in the SingleSignOnService element in the SAML metadata of the identity provider. Example: <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="{samlIdp}"/>`,
|
||||
title: `The entityID of the SAML identity provider. It can typically be found in the EntityID attribute of the EntityDescriptor element in the SAML metadata of the identity provider. Example: <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="{samlEntity}">`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button, Popover, Spin, Tooltip } from 'antd';
|
||||
import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
|
||||
import cx from 'classnames';
|
||||
import { OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import { useTraceActions } from 'hooks/trace/useTraceActions';
|
||||
import {
|
||||
@@ -124,7 +125,7 @@ export default function AttributeActions({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="action-btn">
|
||||
<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' : ''}`}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
padding-block: 12px;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
@@ -25,8 +25,10 @@
|
||||
gap: 8px;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
padding: 2px 12px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-slate-500);
|
||||
.action-btn {
|
||||
display: flex;
|
||||
}
|
||||
@@ -81,22 +83,23 @@
|
||||
|
||||
.action-btn {
|
||||
display: none;
|
||||
|
||||
&--is-open {
|
||||
display: flex;
|
||||
}
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
gap: 4px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
|
||||
.filter-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
border-color: var(--bg-slate-400);
|
||||
box-shadow: none;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-slate-400);
|
||||
background: var(--bg-slate-500);
|
||||
padding: 4px;
|
||||
gap: 3px;
|
||||
height: 24px;
|
||||
@@ -129,7 +132,7 @@
|
||||
gap: 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-slate-400);
|
||||
background-color: var(--bg-slate-400) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +145,7 @@
|
||||
.ant-popover-inner {
|
||||
padding: 8px;
|
||||
min-width: 160px;
|
||||
background: var(--bg-slate-500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +153,9 @@
|
||||
.attributes-corner {
|
||||
.attributes-container {
|
||||
.item {
|
||||
&:hover {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
}
|
||||
.item-key {
|
||||
color: var(--bg-ink-100);
|
||||
}
|
||||
@@ -163,8 +170,6 @@
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
|
||||
.filter-btn {
|
||||
background: var(--bg-vanilla-200);
|
||||
|
||||
|
||||
@@ -48,12 +48,52 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 10px 12px;
|
||||
padding-block: 12px;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
padding: 2px 12px;
|
||||
|
||||
&--interactive {
|
||||
&:hover {
|
||||
background-color: var(--bg-slate-500);
|
||||
.action-btn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: none;
|
||||
&--is-open {
|
||||
display: flex;
|
||||
}
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
gap: 4px;
|
||||
|
||||
.filter-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-color: var(--bg-slate-400);
|
||||
box-shadow: none;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-slate-500);
|
||||
padding: 4px;
|
||||
gap: 3px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-slate-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attribute-key {
|
||||
color: var(--bg-vanilla-400);
|
||||
@@ -238,6 +278,18 @@
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
|
||||
.filter-btn {
|
||||
background: var(--bg-vanilla-200);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.value-wrapper {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-300);
|
||||
|
||||
@@ -16,6 +16,7 @@ import Attributes from './Attributes/Attributes';
|
||||
import { RelatedSignalsViews } from './constants';
|
||||
import Events from './Events/Events';
|
||||
import LinkedSpans from './LinkedSpans/LinkedSpans';
|
||||
import SpanFieldActions from './SpanFieldActions/SpanFieldActions';
|
||||
import SpanRelatedSignals from './SpanRelatedSignals/SpanRelatedSignals';
|
||||
|
||||
interface ISpanDetailsDrawerProps {
|
||||
@@ -141,7 +142,7 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
|
||||
{selectedSpan && !isSpanDetailsDocked && (
|
||||
<>
|
||||
<section className="description">
|
||||
<div className="item">
|
||||
<div className="item item--interactive">
|
||||
<Typography.Text className="attribute-key">span name</Typography.Text>
|
||||
<Tooltip title={selectedSpan.name}>
|
||||
<div className="value-wrapper">
|
||||
@@ -150,14 +151,22 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<SpanFieldActions
|
||||
fieldDisplayName="span name"
|
||||
fieldValue={selectedSpan.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="item">
|
||||
<div className="item item--interactive">
|
||||
<Typography.Text className="attribute-key">span id</Typography.Text>
|
||||
<div className="value-wrapper">
|
||||
<Typography.Text className="attribute-value">
|
||||
{selectedSpan.spanId}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<SpanFieldActions
|
||||
fieldDisplayName="span id"
|
||||
fieldValue={selectedSpan.spanId}
|
||||
/>
|
||||
</div>
|
||||
<div className="item">
|
||||
<Typography.Text className="attribute-key">start time</Typography.Text>
|
||||
@@ -167,15 +176,19 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="item">
|
||||
<div className="item item--interactive">
|
||||
<Typography.Text className="attribute-key">duration</Typography.Text>
|
||||
<div className="value-wrapper">
|
||||
<Typography.Text className="attribute-value">
|
||||
{getYAxisFormattedValue(`${selectedSpan.durationNano}`, 'ns')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<SpanFieldActions
|
||||
fieldDisplayName="duration"
|
||||
fieldValue={selectedSpan.durationNano.toString()}
|
||||
/>
|
||||
</div>
|
||||
<div className="item">
|
||||
<div className="item item--interactive">
|
||||
<Typography.Text className="attribute-key">service</Typography.Text>
|
||||
<div className="service">
|
||||
<div className="dot" style={{ backgroundColor: color }} />
|
||||
@@ -187,16 +200,24 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<SpanFieldActions
|
||||
fieldDisplayName="service"
|
||||
fieldValue={selectedSpan.serviceName}
|
||||
/>
|
||||
</div>
|
||||
<div className="item">
|
||||
<div className="item item--interactive">
|
||||
<Typography.Text className="attribute-key">span kind</Typography.Text>
|
||||
<div className="value-wrapper">
|
||||
<Typography.Text className="attribute-value">
|
||||
{selectedSpan.spanKind}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<SpanFieldActions
|
||||
fieldDisplayName="span kind"
|
||||
fieldValue={selectedSpan.spanKind}
|
||||
/>
|
||||
</div>
|
||||
<div className="item">
|
||||
<div className="item item--interactive">
|
||||
<Typography.Text className="attribute-key">
|
||||
status code string
|
||||
</Typography.Text>
|
||||
@@ -205,10 +226,14 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
|
||||
{selectedSpan.statusCodeString}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<SpanFieldActions
|
||||
fieldDisplayName="status code string"
|
||||
fieldValue={selectedSpan.statusCodeString}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedSpan.statusMessage && (
|
||||
<div className="item">
|
||||
<div className="item item--interactive">
|
||||
<Typography.Text className="attribute-key">
|
||||
status message
|
||||
</Typography.Text>
|
||||
@@ -217,6 +242,10 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
|
||||
{selectedSpan.statusMessage}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<SpanFieldActions
|
||||
fieldDisplayName="status message"
|
||||
fieldValue={selectedSpan.statusMessage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="item">
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { Button, Popover, Spin, Tooltip } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import { useTraceActions } from 'hooks/trace/useTraceActions';
|
||||
import { ArrowDownToDot, ArrowUpFromDot, Copy, Ellipsis } from 'lucide-react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
// Field mapping from display names to actual span property keys
|
||||
const SPAN_FIELD_MAPPING: Record<string, string> = {
|
||||
'span name': 'name',
|
||||
'span id': 'span_id',
|
||||
duration: 'durationNano',
|
||||
service: 'serviceName',
|
||||
'span kind': 'spanKind',
|
||||
'status code string': 'statusCodeString',
|
||||
'status message': 'statusMessage',
|
||||
};
|
||||
|
||||
interface SpanFieldActionsProps {
|
||||
fieldDisplayName: string;
|
||||
fieldValue: string;
|
||||
}
|
||||
|
||||
export default function SpanFieldActions({
|
||||
fieldDisplayName,
|
||||
fieldValue,
|
||||
}: SpanFieldActionsProps): JSX.Element {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [isFilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
|
||||
const [isFilterOutLoading, setIsFilterOutLoading] = useState<boolean>(false);
|
||||
|
||||
const { onAddToQuery, onCopyFieldName, onCopyFieldValue } = useTraceActions();
|
||||
|
||||
const mappedFieldKey =
|
||||
SPAN_FIELD_MAPPING[fieldDisplayName] || fieldDisplayName;
|
||||
|
||||
const handleFilter = useCallback(
|
||||
async (operator: string, isFilterIn: boolean): Promise<void> => {
|
||||
const isLoading = isFilterIn ? isFilterInLoading : isFilterOutLoading;
|
||||
const setLoading = isFilterIn ? setIsFilterInLoading : setIsFilterOutLoading;
|
||||
|
||||
if (!onAddToQuery || isLoading) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await onAddToQuery(mappedFieldKey, fieldValue, operator);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
onAddToQuery,
|
||||
mappedFieldKey,
|
||||
fieldValue,
|
||||
isFilterInLoading,
|
||||
isFilterOutLoading,
|
||||
],
|
||||
);
|
||||
|
||||
const handleFilterIn = useCallback(() => handleFilter(OPERATORS['='], true), [
|
||||
handleFilter,
|
||||
]);
|
||||
|
||||
const handleFilterOut = useCallback(
|
||||
() => handleFilter(OPERATORS['!='], false),
|
||||
[handleFilter],
|
||||
);
|
||||
|
||||
const handleCopy = useCallback(
|
||||
(copyFn: ((value: string) => void) | undefined, value: string): void => {
|
||||
if (copyFn) {
|
||||
copyFn(value);
|
||||
}
|
||||
setIsOpen(false);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleCopyFieldName = useCallback(
|
||||
() => handleCopy(onCopyFieldName, mappedFieldKey),
|
||||
[handleCopy, onCopyFieldName, mappedFieldKey],
|
||||
);
|
||||
|
||||
const handleCopyFieldValue = useCallback(
|
||||
() => handleCopy(onCopyFieldValue, fieldValue),
|
||||
[fieldValue, handleCopy, onCopyFieldValue],
|
||||
);
|
||||
|
||||
const moreActionsContent = (
|
||||
<div className="attribute-actions-menu">
|
||||
<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="Filter for value">
|
||||
<Button
|
||||
className="filter-btn periscope-btn"
|
||||
aria-label="Filter for value"
|
||||
disabled={isFilterInLoading}
|
||||
icon={
|
||||
isFilterInLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} />
|
||||
)
|
||||
}
|
||||
onClick={handleFilterIn}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Filter out value">
|
||||
<Button
|
||||
className="filter-btn periscope-btn"
|
||||
aria-label="Filter out value"
|
||||
disabled={isFilterOutLoading}
|
||||
icon={
|
||||
isFilterOutLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<ArrowUpFromDot size={14} style={{ transform: 'rotate(90deg)' }} />
|
||||
)
|
||||
}
|
||||
onClick={handleFilterOut}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
arrow={false}
|
||||
content={moreActionsContent}
|
||||
rootClassName="attribute-actions-content"
|
||||
trigger="hover"
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<Button
|
||||
icon={<Ellipsis size={14} />}
|
||||
className="filter-btn periscope-btn"
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,659 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { AppProvider } from 'providers/App/App';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import { MemoryRouter, Route } from 'react-router-dom';
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
|
||||
import SpanDetailsDrawer from '../SpanDetailsDrawer';
|
||||
|
||||
// Mock external dependencies following the same pattern as AttributeActions tests
|
||||
const mockRedirectWithQueryBuilderData = jest.fn();
|
||||
const mockNotifications = {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
const mockSetCopy = jest.fn();
|
||||
const mockQueryClient = {
|
||||
fetchQuery: jest.fn(),
|
||||
};
|
||||
|
||||
// Mock the hooks - same as AttributeActions test setup
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: (): any => ({
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: { key: 'signoz_span_duration' },
|
||||
filters: { items: [], op: 'AND' },
|
||||
filter: { expression: '' },
|
||||
groupBy: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
redirectWithQueryBuilderData: mockRedirectWithQueryBuilderData,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
useNotifications: (): any => ({ notifications: mockNotifications }),
|
||||
}));
|
||||
|
||||
jest.mock('react-use', () => ({
|
||||
...jest.requireActual('react-use'),
|
||||
useCopyToClipboard: (): any => [{ value: '' }, mockSetCopy],
|
||||
}));
|
||||
|
||||
jest.mock('react-query', () => ({
|
||||
...jest.requireActual('react-query'),
|
||||
useQueryClient: (): any => mockQueryClient,
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/sonner', () => ({ toast: jest.fn() }));
|
||||
|
||||
// Mock the API response for getAggregateKeys
|
||||
const mockAggregateKeysResponse = {
|
||||
payload: {
|
||||
attributeKeys: [
|
||||
{
|
||||
key: 'name',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
},
|
||||
{
|
||||
key: 'serviceName',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
isColumn: true,
|
||||
},
|
||||
{
|
||||
key: 'spanKind',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
},
|
||||
{
|
||||
key: 'statusCodeString',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
},
|
||||
{
|
||||
key: 'span_id',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
},
|
||||
{
|
||||
key: 'durationNano',
|
||||
dataType: 'number',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockQueryClient.fetchQuery.mockResolvedValue(mockAggregateKeysResponse);
|
||||
});
|
||||
|
||||
// Create realistic mock span data for testing
|
||||
const createMockSpan = (overrides: Partial<Span> = {}): Span => ({
|
||||
spanId: '28a8a67365d0bd8b',
|
||||
traceId: '000000000000000071dc9b0a338729b4',
|
||||
name: 'HTTP GET /api/users',
|
||||
timestamp: 1699872000000000,
|
||||
durationNano: 150000000,
|
||||
serviceName: 'frontend-service',
|
||||
spanKind: 'server',
|
||||
statusCodeString: 'OK',
|
||||
statusMessage: '',
|
||||
tagMap: {
|
||||
'http.method': 'GET',
|
||||
'http.url': '/api/users?page=1',
|
||||
},
|
||||
event: [],
|
||||
references: [],
|
||||
hasError: false,
|
||||
rootSpanId: '',
|
||||
parentSpanId: '',
|
||||
kind: 0,
|
||||
rootName: '',
|
||||
hasChildren: false,
|
||||
hasSibling: false,
|
||||
subTreeNodeCount: 0,
|
||||
level: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
interface RenderResult {
|
||||
user: ReturnType<typeof userEvent.setup>;
|
||||
}
|
||||
|
||||
const renderSpanDetailsDrawer = (
|
||||
span: Span = createMockSpan(),
|
||||
): RenderResult => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<AppProvider>
|
||||
<MemoryRouter>
|
||||
<Route>
|
||||
<SpanDetailsDrawer
|
||||
isSpanDetailsDocked={false}
|
||||
setIsSpanDetailsDocked={jest.fn()}
|
||||
selectedSpan={span}
|
||||
traceStartTime={span.timestamp}
|
||||
traceEndTime={span.timestamp + span.durationNano}
|
||||
/>
|
||||
</Route>
|
||||
</MemoryRouter>
|
||||
</AppProvider>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
return { user };
|
||||
};
|
||||
|
||||
describe('SpanFieldActions User Flow Tests', () => {
|
||||
describe('Primary Filter Flow', () => {
|
||||
it('should allow user to filter for span name value and navigate to traces explorer', async () => {
|
||||
const testSpan = createMockSpan({
|
||||
name: 'GET /api/orders',
|
||||
});
|
||||
const { user } = renderSpanDetailsDrawer(testSpan);
|
||||
|
||||
// User sees the span name displayed
|
||||
expect(screen.getByText('span name')).toBeInTheDocument();
|
||||
expect(screen.getByText('GET /api/orders')).toBeInTheDocument();
|
||||
|
||||
// Find the span name field item
|
||||
const spanNameItem = screen.getByText('span name').closest('.item');
|
||||
expect(spanNameItem).toBeInTheDocument();
|
||||
|
||||
// User hovers over the span name field to reveal action buttons
|
||||
await user.hover(spanNameItem!);
|
||||
|
||||
// Action buttons should appear on hover
|
||||
const actionButtons = spanNameItem!.querySelector('.action-btn');
|
||||
expect(actionButtons).toBeInTheDocument();
|
||||
|
||||
const filterForButton = spanNameItem!.querySelector(
|
||||
'[aria-label="Filter for value"]',
|
||||
) as HTMLElement;
|
||||
expect(filterForButton).toBeInTheDocument();
|
||||
|
||||
// User clicks "Filter for value" button
|
||||
await user.click(filterForButton);
|
||||
|
||||
// Verify navigation to traces explorer with correct filter
|
||||
await waitFor(() => {
|
||||
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
builder: expect.objectContaining({
|
||||
queryData: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
dataSource: 'traces',
|
||||
filters: expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: 'name' }),
|
||||
op: '=',
|
||||
value: 'GET /api/orders',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
{},
|
||||
ROUTES.TRACES_EXPLORER,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow user to filter for service name with proper field mapping', async () => {
|
||||
const testSpan = createMockSpan({
|
||||
serviceName: 'payment-service',
|
||||
});
|
||||
const { user } = renderSpanDetailsDrawer(testSpan);
|
||||
|
||||
// User sees the service displayed
|
||||
expect(screen.getByText('service')).toBeInTheDocument();
|
||||
expect(screen.getByText('payment-service')).toBeInTheDocument();
|
||||
|
||||
// Find the service field item
|
||||
const serviceItem = screen.getByText('service').closest('.item');
|
||||
expect(serviceItem).toBeInTheDocument();
|
||||
|
||||
// User hovers and clicks filter for
|
||||
await user.hover(serviceItem!);
|
||||
const filterForButton = serviceItem!.querySelector(
|
||||
'[aria-label="Filter for value"]',
|
||||
) as HTMLElement;
|
||||
await user.click(filterForButton);
|
||||
|
||||
// Verify correct field mapping: "service" display name → "serviceName" query key
|
||||
await waitFor(() => {
|
||||
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
builder: expect.objectContaining({
|
||||
queryData: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
dataSource: 'traces',
|
||||
filters: expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: 'serviceName' }),
|
||||
op: '=',
|
||||
value: 'payment-service',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
{},
|
||||
ROUTES.TRACES_EXPLORER,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filter Out Flow', () => {
|
||||
it('should allow user to exclude span kind value and navigate to traces explorer', async () => {
|
||||
const testSpan = createMockSpan({
|
||||
spanKind: 'client',
|
||||
});
|
||||
const { user } = renderSpanDetailsDrawer(testSpan);
|
||||
|
||||
// User sees the span kind displayed
|
||||
expect(screen.getByText('span kind')).toBeInTheDocument();
|
||||
expect(screen.getByText('client')).toBeInTheDocument();
|
||||
|
||||
// Find the span kind field item
|
||||
const spanKindItem = screen.getByText('span kind').closest('.item');
|
||||
expect(spanKindItem).toBeInTheDocument();
|
||||
|
||||
// User hovers over the span kind field
|
||||
await user.hover(spanKindItem!);
|
||||
|
||||
const filterOutButton = spanKindItem!.querySelector(
|
||||
'[aria-label="Filter out value"]',
|
||||
) as HTMLElement;
|
||||
expect(filterOutButton).toBeInTheDocument();
|
||||
|
||||
// User clicks "Filter out value" button
|
||||
await user.click(filterOutButton);
|
||||
|
||||
// Verify navigation to traces explorer with exclusion filter
|
||||
await waitFor(() => {
|
||||
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
builder: expect.objectContaining({
|
||||
queryData: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
dataSource: 'traces',
|
||||
filters: expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: 'spanKind' }),
|
||||
op: '!=',
|
||||
value: 'client',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
{},
|
||||
ROUTES.TRACES_EXPLORER,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Copy Actions Flow', () => {
|
||||
it('should allow user to copy field name and field value through popover actions', async () => {
|
||||
const testSpan = createMockSpan({
|
||||
statusCodeString: 'ERROR',
|
||||
});
|
||||
const { user } = renderSpanDetailsDrawer(testSpan);
|
||||
|
||||
// User sees the status code string displayed
|
||||
expect(screen.getByText('status code string')).toBeInTheDocument();
|
||||
expect(screen.getByText('ERROR')).toBeInTheDocument();
|
||||
|
||||
// Find the status code string field item
|
||||
const statusCodeItem = screen
|
||||
.getByText('status code string')
|
||||
.closest('.item');
|
||||
expect(statusCodeItem).toBeInTheDocument();
|
||||
|
||||
// User hovers over the field to reveal action buttons
|
||||
await user.hover(statusCodeItem!);
|
||||
|
||||
// User clicks the more actions button (ellipsis)
|
||||
const moreActionsButton = statusCodeItem!
|
||||
.querySelector('.lucide-ellipsis')
|
||||
?.closest('button') as HTMLElement;
|
||||
expect(moreActionsButton).toBeInTheDocument();
|
||||
await user.click(moreActionsButton);
|
||||
|
||||
// Verify popover opens with copy options
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Copy Field Name')).toBeInTheDocument();
|
||||
expect(screen.getByText('Copy Field Value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// User clicks "Copy Field Name"
|
||||
const copyFieldNameButton = screen.getByText('Copy Field Name');
|
||||
fireEvent.click(copyFieldNameButton);
|
||||
|
||||
// Verify field name is copied with correct mapping
|
||||
await waitFor(() => {
|
||||
expect(mockSetCopy).toHaveBeenCalledWith('statusCodeString');
|
||||
expect(mockNotifications.success).toHaveBeenCalledWith({
|
||||
message: 'Field name copied to clipboard',
|
||||
});
|
||||
});
|
||||
|
||||
// Reset mocks and test copy field value
|
||||
mockSetCopy.mockClear();
|
||||
mockNotifications.success.mockClear();
|
||||
|
||||
// Open popover again for copy field value test
|
||||
await user.hover(statusCodeItem!);
|
||||
await user.click(moreActionsButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Copy Field Value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// User clicks "Copy Field Value"
|
||||
const copyFieldValueButton = screen.getByText('Copy Field Value');
|
||||
fireEvent.click(copyFieldValueButton);
|
||||
|
||||
// Verify field value is copied
|
||||
await waitFor(() => {
|
||||
expect(mockSetCopy).toHaveBeenCalledWith('ERROR');
|
||||
expect(mockNotifications.success).toHaveBeenCalledWith({
|
||||
message: 'Field value copied to clipboard',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple Standard Fields', () => {
|
||||
it('should work consistently across different field types with proper field mappings', async () => {
|
||||
const testSpan = createMockSpan({
|
||||
spanId: 'abc123def456',
|
||||
name: 'Database Query',
|
||||
serviceName: 'db-service',
|
||||
spanKind: 'internal',
|
||||
statusCodeString: 'OK',
|
||||
});
|
||||
const { user } = renderSpanDetailsDrawer(testSpan);
|
||||
|
||||
// Test span ID field with its mapping
|
||||
const spanIdItem = screen.getByText('span id').closest('.item');
|
||||
await user.hover(spanIdItem!);
|
||||
const spanIdFilterButton = spanIdItem!.querySelector(
|
||||
'[aria-label="Filter for value"]',
|
||||
) as HTMLElement;
|
||||
await user.click(spanIdFilterButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
builder: expect.objectContaining({
|
||||
queryData: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
filters: expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: 'span_id' }),
|
||||
op: '=',
|
||||
value: 'abc123def456',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
{},
|
||||
ROUTES.TRACES_EXPLORER,
|
||||
);
|
||||
});
|
||||
|
||||
mockRedirectWithQueryBuilderData.mockClear();
|
||||
|
||||
// Test span name field
|
||||
const spanNameItem = screen.getByText('span name').closest('.item');
|
||||
await user.hover(spanNameItem!);
|
||||
const spanNameFilterButton = spanNameItem!.querySelector(
|
||||
'[aria-label="Filter for value"]',
|
||||
) as HTMLElement;
|
||||
await user.click(spanNameFilterButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
builder: expect.objectContaining({
|
||||
queryData: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
filters: expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: 'name' }),
|
||||
op: '=',
|
||||
value: 'Database Query',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
{},
|
||||
ROUTES.TRACES_EXPLORER,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Special Field Values', () => {
|
||||
it('should handle duration field with numeric values properly', async () => {
|
||||
const testSpan = createMockSpan({
|
||||
durationNano: 250000000, // 250ms
|
||||
});
|
||||
const { user } = renderSpanDetailsDrawer(testSpan);
|
||||
|
||||
// User sees the duration displayed (formatted)
|
||||
expect(screen.getByText('duration')).toBeInTheDocument();
|
||||
// Duration should be formatted by getYAxisFormattedValue, but we test the raw value is used in filter
|
||||
|
||||
// Find the duration field item
|
||||
const durationItem = screen.getByText('duration').closest('.item');
|
||||
expect(durationItem).toBeInTheDocument();
|
||||
|
||||
// User hovers and clicks filter for
|
||||
await user.hover(durationItem!);
|
||||
const filterForButton = durationItem!.querySelector(
|
||||
'[aria-label="Filter for value"]',
|
||||
) as HTMLElement;
|
||||
await user.click(filterForButton);
|
||||
|
||||
// Verify the raw numeric value is used in the filter, not the formatted display value
|
||||
await waitFor(() => {
|
||||
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
builder: expect.objectContaining({
|
||||
queryData: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
dataSource: 'traces',
|
||||
filters: expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: 'durationNano' }),
|
||||
op: '=',
|
||||
value: '250000000', // Raw numeric value as string
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
{},
|
||||
ROUTES.TRACES_EXPLORER,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle fields with special characters and preserve exact field values', async () => {
|
||||
const testSpan = createMockSpan({
|
||||
name: 'POST /api/users/create',
|
||||
statusCodeString: '"INTERNAL_ERROR"', // Quoted value to test exact preservation
|
||||
});
|
||||
const { user } = renderSpanDetailsDrawer(testSpan);
|
||||
|
||||
// Test span name with special characters
|
||||
const spanNameItem = screen.getByText('span name').closest('.item');
|
||||
await user.hover(spanNameItem!);
|
||||
const moreActionsButton = spanNameItem!
|
||||
.querySelector('.lucide-ellipsis')
|
||||
?.closest('button') as HTMLElement;
|
||||
await user.click(moreActionsButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Copy Field Value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const copyFieldValueButton = screen.getByText('Copy Field Value');
|
||||
fireEvent.click(copyFieldValueButton);
|
||||
|
||||
// Verify special characters are handled correctly
|
||||
await waitFor(() => {
|
||||
expect(mockSetCopy).toHaveBeenCalledWith('POST /api/users/create');
|
||||
});
|
||||
|
||||
// Reset and test quoted value handling
|
||||
mockSetCopy.mockClear();
|
||||
|
||||
// Test status code string with quotes (should preserve exact value)
|
||||
const statusItem = screen.getByText('status code string').closest('.item');
|
||||
await user.hover(statusItem!);
|
||||
const statusMoreActionsButton = statusItem!
|
||||
.querySelector('.lucide-ellipsis')
|
||||
?.closest('button') as HTMLElement;
|
||||
await user.click(statusMoreActionsButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Copy Field Value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const statusCopyButton = screen.getByText('Copy Field Value');
|
||||
fireEvent.click(statusCopyButton);
|
||||
|
||||
// Verify exact field value is preserved (including quotes)
|
||||
await waitFor(() => {
|
||||
expect(mockSetCopy).toHaveBeenCalledWith('"INTERNAL_ERROR"');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle status message field with action buttons when present', async () => {
|
||||
const testSpan = createMockSpan({
|
||||
statusMessage: 'Connection timeout error',
|
||||
});
|
||||
const { user } = renderSpanDetailsDrawer(testSpan);
|
||||
|
||||
// User sees the status message displayed
|
||||
expect(screen.getByText('status message')).toBeInTheDocument();
|
||||
expect(screen.getByText('Connection timeout error')).toBeInTheDocument();
|
||||
|
||||
// Find the status message field item
|
||||
const statusMessageItem = screen
|
||||
.getByText('status message')
|
||||
.closest('.item');
|
||||
expect(statusMessageItem).toBeInTheDocument();
|
||||
|
||||
// User hovers over the status message field to reveal action buttons
|
||||
await user.hover(statusMessageItem!);
|
||||
|
||||
const filterForButton = statusMessageItem!.querySelector(
|
||||
'[aria-label="Filter for value"]',
|
||||
) as HTMLElement;
|
||||
expect(filterForButton).toBeInTheDocument();
|
||||
|
||||
// User clicks "Filter for value" button
|
||||
await user.click(filterForButton);
|
||||
|
||||
// Verify navigation to traces explorer with correct filter mapping
|
||||
await waitFor(() => {
|
||||
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
builder: expect.objectContaining({
|
||||
queryData: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
dataSource: 'traces',
|
||||
filters: expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: 'statusMessage' }),
|
||||
op: '=',
|
||||
value: 'Connection timeout error',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
{},
|
||||
ROUTES.TRACES_EXPLORER,
|
||||
);
|
||||
});
|
||||
|
||||
// Reset and test copy functionality
|
||||
mockRedirectWithQueryBuilderData.mockClear();
|
||||
|
||||
// Test copy field name functionality
|
||||
await user.hover(statusMessageItem!);
|
||||
const moreActionsButton = statusMessageItem!
|
||||
.querySelector('.lucide-ellipsis')
|
||||
?.closest('button') as HTMLElement;
|
||||
await user.click(moreActionsButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Copy Field Name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const copyFieldNameButton = screen.getByText('Copy Field Name');
|
||||
fireEvent.click(copyFieldNameButton);
|
||||
|
||||
// Verify field name mapping (display "status message" → query "statusMessage")
|
||||
await waitFor(() => {
|
||||
expect(mockSetCopy).toHaveBeenCalledWith('statusMessage');
|
||||
expect(mockNotifications.success).toHaveBeenCalledWith({
|
||||
message: 'Field name copied to clipboard',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -36,6 +36,7 @@ import { getYAxisScale } from './utils/getYAxisScale';
|
||||
interface ExtendedUPlot extends uPlot {
|
||||
_legendScrollCleanup?: () => void;
|
||||
_tooltipCleanup?: () => void;
|
||||
_legendElementCleanup?: Array<() => void>;
|
||||
}
|
||||
|
||||
export interface GetUPlotChartOptions {
|
||||
@@ -473,6 +474,9 @@ export const getUPlotChartOptions = ({
|
||||
if (legend) {
|
||||
const legendElement = legend as HTMLElement;
|
||||
|
||||
// Initialize cleanup array for legend element listeners
|
||||
(self as ExtendedUPlot)._legendElementCleanup = [];
|
||||
|
||||
// Apply enhanced legend styling
|
||||
if (enhancedLegend) {
|
||||
applyEnhancedLegendStyling(
|
||||
@@ -639,6 +643,17 @@ export const getUPlotChartOptions = ({
|
||||
thElement.addEventListener('mouseenter', showTooltip);
|
||||
thElement.addEventListener('mouseleave', hideTooltip);
|
||||
|
||||
// Store cleanup function for tooltip listeners
|
||||
(self as ExtendedUPlot)._legendElementCleanup?.push(() => {
|
||||
thElement.removeEventListener('mouseenter', showTooltip);
|
||||
thElement.removeEventListener('mouseleave', hideTooltip);
|
||||
// Cleanup any lingering tooltip
|
||||
if (tooltipElement) {
|
||||
tooltipElement.remove();
|
||||
tooltipElement = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Add click handlers for marker and text separately
|
||||
const currentMarker = thElement.querySelector('.u-marker');
|
||||
const textElement = thElement.querySelector('.legend-text');
|
||||
@@ -658,7 +673,7 @@ export const getUPlotChartOptions = ({
|
||||
|
||||
// Marker click handler - checkbox behavior (toggle individual series)
|
||||
if (currentMarker) {
|
||||
currentMarker.addEventListener('click', (e) => {
|
||||
const markerClickHandler = (e: Event): void => {
|
||||
e.stopPropagation?.(); // Prevent event bubbling to text handler
|
||||
|
||||
if (stackChart) {
|
||||
@@ -680,12 +695,19 @@ export const getUPlotChartOptions = ({
|
||||
return newGraphVisibilityStates;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
currentMarker.addEventListener('click', markerClickHandler);
|
||||
|
||||
// Store cleanup function for marker click listener
|
||||
(self as ExtendedUPlot)._legendElementCleanup?.push(() => {
|
||||
currentMarker.removeEventListener('click', markerClickHandler);
|
||||
});
|
||||
}
|
||||
|
||||
// Text click handler - show only/show all behavior (existing behavior)
|
||||
if (textElement) {
|
||||
textElement.addEventListener('click', (e) => {
|
||||
const textClickHandler = (e: Event): void => {
|
||||
e.stopPropagation?.(); // Prevent event bubbling
|
||||
|
||||
if (stackChart) {
|
||||
@@ -716,6 +738,13 @@ export const getUPlotChartOptions = ({
|
||||
return newGraphVisibilityStates;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
textElement.addEventListener('click', textClickHandler);
|
||||
|
||||
// Store cleanup function for text click listener
|
||||
(self as ExtendedUPlot)._legendElementCleanup?.push(() => {
|
||||
textElement.removeEventListener('click', textClickHandler);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -723,6 +752,33 @@ export const getUPlotChartOptions = ({
|
||||
}
|
||||
},
|
||||
],
|
||||
destroy: [
|
||||
(self): void => {
|
||||
// Clean up legend scroll listener
|
||||
if ((self as ExtendedUPlot)._legendScrollCleanup) {
|
||||
(self as ExtendedUPlot)._legendScrollCleanup?.();
|
||||
(self as ExtendedUPlot)._legendScrollCleanup = undefined;
|
||||
}
|
||||
|
||||
// Clean up tooltip global listener
|
||||
if ((self as ExtendedUPlot)._tooltipCleanup) {
|
||||
(self as ExtendedUPlot)._tooltipCleanup?.();
|
||||
(self as ExtendedUPlot)._tooltipCleanup = undefined;
|
||||
}
|
||||
|
||||
// Clean up all legend element listeners
|
||||
if ((self as ExtendedUPlot)._legendElementCleanup) {
|
||||
(self as ExtendedUPlot)._legendElementCleanup?.forEach((cleanup) => {
|
||||
cleanup();
|
||||
});
|
||||
(self as ExtendedUPlot)._legendElementCleanup = [];
|
||||
}
|
||||
|
||||
// Clean up any remaining tooltips in DOM
|
||||
const existingTooltips = document.querySelectorAll('.legend-tooltip');
|
||||
existingTooltips.forEach((tooltip) => tooltip.remove());
|
||||
},
|
||||
],
|
||||
},
|
||||
series: customSeries
|
||||
? customSeries(apiResponse?.data?.result || [])
|
||||
|
||||
@@ -700,24 +700,23 @@ describe('TracesExplorer - ', () => {
|
||||
});
|
||||
|
||||
it('select a view options - assert and save this view', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const { container } = renderWithTracesExplorerRouter(<TracesExplorer />, [
|
||||
'/traces-explorer/?panelType=list&selectedExplorerView=list',
|
||||
]);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
await act(async () => {
|
||||
fireEvent.mouseDown(
|
||||
container.querySelector(
|
||||
'.view-options .ant-select-selection-search-input',
|
||||
) as HTMLElement,
|
||||
);
|
||||
});
|
||||
|
||||
const viewListOptions = await screen.findByRole('listbox');
|
||||
expect(viewListOptions).toBeInTheDocument();
|
||||
const viewSearchInput = container.querySelector(
|
||||
'.view-options .ant-select-selection-search-input',
|
||||
) as HTMLElement;
|
||||
|
||||
expect(within(viewListOptions).getByText('R-test panel')).toBeInTheDocument();
|
||||
expect(viewSearchInput).toBeInTheDocument();
|
||||
|
||||
expect(within(viewListOptions).getByText('Table View')).toBeInTheDocument();
|
||||
fireEvent.mouseDown(viewSearchInput);
|
||||
|
||||
expect(
|
||||
await screen.findByRole('option', { name: 'R-test panel' }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// save this view
|
||||
fireEvent.click(screen.getByText('Save this view'));
|
||||
|
||||
@@ -13752,10 +13752,10 @@ on-finished@2.4.1, on-finished@^2.4.1:
|
||||
dependencies:
|
||||
ee-first "1.1.1"
|
||||
|
||||
on-headers@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz"
|
||||
integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
|
||||
on-headers@^1.1.0, on-headers@~1.0.2:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65"
|
||||
integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==
|
||||
|
||||
once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
|
||||
7
go.mod
7
go.mod
@@ -32,7 +32,6 @@ require (
|
||||
github.com/knadh/koanf v1.5.0
|
||||
github.com/knadh/koanf/v2 v2.2.0
|
||||
github.com/mailru/easyjson v0.7.7
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/open-telemetry/opamp-go v0.19.0
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.128.0
|
||||
github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67
|
||||
@@ -84,6 +83,7 @@ require (
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/apimachinery v0.34.0
|
||||
modernc.org/sqlite v1.39.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -93,10 +93,9 @@ require (
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.39.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -330,7 +329,7 @@ require (
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
gonum.org/v1/gonum v0.16.0 // indirect
|
||||
|
||||
26
go.sum
26
go.sum
@@ -680,8 +680,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
@@ -1461,8 +1459,8 @@ golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
@@ -1785,18 +1783,18 @@ k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOP
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -1805,8 +1803,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
|
||||
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
|
||||
modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -38,7 +38,7 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/contextlinks"
|
||||
traceFunnelsModule "github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
@@ -23,6 +25,12 @@ type PostgresConfig struct {
|
||||
type SqliteConfig struct {
|
||||
// Path is the path to the sqlite database.
|
||||
Path string `mapstructure:"path"`
|
||||
|
||||
// Mode is the mode to use for the sqlite database.
|
||||
Mode string `mapstructure:"mode"`
|
||||
|
||||
// BusyTimeout is the timeout for the sqlite database to wait for a lock.
|
||||
BusyTimeout time.Duration `mapstructure:"busy_timeout"`
|
||||
}
|
||||
|
||||
type ConnectionConfig struct {
|
||||
@@ -41,7 +49,9 @@ func newConfig() factory.Config {
|
||||
MaxOpenConns: 100,
|
||||
},
|
||||
Sqlite: SqliteConfig{
|
||||
Path: "/var/lib/signoz/signoz.db",
|
||||
Path: "/var/lib/signoz/signoz.db",
|
||||
Mode: "delete",
|
||||
BusyTimeout: 10000 * time.Millisecond, // increasing the defaults from https://github.com/mattn/go-sqlite3/blob/master/sqlite3.go#L1098 because of transpilation from C to GO
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,17 @@ package sqlitesqlstore
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
sqlite3 "github.com/mattn/go-sqlite3"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||
|
||||
"modernc.org/sqlite"
|
||||
sqlite3 "modernc.org/sqlite/lib"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
@@ -38,7 +42,12 @@ func NewFactory(hookFactories ...factory.ProviderFactory[sqlstore.SQLStoreHook,
|
||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config sqlstore.Config, hooks ...sqlstore.SQLStoreHook) (sqlstore.SQLStore, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/sqlitesqlstore")
|
||||
|
||||
sqldb, err := sql.Open("sqlite3", "file:"+config.Sqlite.Path+"?_foreign_keys=true")
|
||||
connectionParams := url.Values{}
|
||||
// do not update the order of the connection params as busy_timeout doesn't work if it's not the first parameter
|
||||
connectionParams.Add("_pragma", fmt.Sprintf("busy_timeout(%d)", config.Sqlite.BusyTimeout.Milliseconds()))
|
||||
connectionParams.Add("_pragma", fmt.Sprintf("journal_mode(%s)", config.Sqlite.Mode))
|
||||
connectionParams.Add("_pragma", "foreign_keys(1)")
|
||||
sqldb, err := sql.Open("sqlite", "file:"+config.Sqlite.Path+"?"+connectionParams.Encode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -82,8 +91,8 @@ func (provider *provider) WrapNotFoundErrf(err error, code errors.Code, format s
|
||||
}
|
||||
|
||||
func (provider *provider) WrapAlreadyExistsErrf(err error, code errors.Code, format string, args ...any) error {
|
||||
if sqlite3Err, ok := err.(sqlite3.Error); ok {
|
||||
if sqlite3Err.ExtendedCode == sqlite3.ErrConstraintUnique {
|
||||
if sqlite3Err, ok := err.(*sqlite.Error); ok {
|
||||
if sqlite3Err.Code() == sqlite3.SQLITE_CONSTRAINT_UNIQUE || sqlite3Err.Code() == sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY {
|
||||
return errors.Wrapf(err, errors.TypeAlreadyExists, code, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ builds:
|
||||
- id: signoz
|
||||
binary: bin/histogram-quantile
|
||||
main: scripts/clickhouse/histogramquantile/main.go
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from os import path
|
||||
import platform
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
@@ -68,9 +69,10 @@ def signoz( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
|
||||
provider = request.config.getoption("--sqlstore-provider")
|
||||
if provider == "sqlite":
|
||||
dir_path = path.dirname(sqlstore.env["SIGNOZ_SQLSTORE_SQLITE_PATH"])
|
||||
container.with_volume_mapping(
|
||||
sqlstore.env["SIGNOZ_SQLSTORE_SQLITE_PATH"],
|
||||
sqlstore.env["SIGNOZ_SQLSTORE_SQLITE_PATH"],
|
||||
dir_path,
|
||||
dir_path,
|
||||
"rw",
|
||||
)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ def sqlite(
|
||||
with engine.connect() as conn:
|
||||
result = conn.execute(sql.text("SELECT 1"))
|
||||
assert result.fetchone()[0] == 1
|
||||
|
||||
|
||||
return types.TestContainerSQL(
|
||||
container=types.TestContainerDocker(
|
||||
@@ -52,13 +53,14 @@ def sqlite(
|
||||
result = conn.execute(sql.text("SELECT 1"))
|
||||
assert result.fetchone()[0] == 1
|
||||
|
||||
|
||||
return types.TestContainerSQL(
|
||||
container=types.TestContainerDocker(
|
||||
id="",
|
||||
host_configs={},
|
||||
container_configs={},
|
||||
),
|
||||
conn=conn,
|
||||
conn=engine,
|
||||
env=cache["env"],
|
||||
)
|
||||
|
||||
|
||||
@@ -131,14 +131,14 @@ def test_refresh_license(
|
||||
|
||||
assert response.status_code == http.HTTPStatus.NO_CONTENT
|
||||
|
||||
with signoz.sqlstore.conn.connect() as conn:
|
||||
result = conn.execute(
|
||||
sql.text("SELECT data FROM license WHERE id=:id"),
|
||||
{"id": "0196360e-90cd-7a74-8313-1aa815ce2a67"},
|
||||
)
|
||||
record = result.fetchone()[0]
|
||||
assert json.loads(record)["valid_from"] == 1732146922
|
||||
|
||||
response = requests.get(
|
||||
url=signoz.self.host_configs["8080"].get("/api/v3/licenses/active"),
|
||||
headers={"Authorization": "Bearer " + access_token},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == http.HTTPStatus.OK
|
||||
assert response.json()["data"]["valid_from"] == 1732146922
|
||||
|
||||
response = requests.post(
|
||||
url=signoz.zeus.host_configs["8080"].get("/__admin/requests/count"),
|
||||
json={"method": "GET", "url": "/v2/licenses/me"},
|
||||
|
||||
@@ -185,6 +185,7 @@ def test_reset_password(
|
||||
assert token is not None
|
||||
|
||||
|
||||
|
||||
def test_reset_password_with_no_password(
|
||||
signoz: types.SigNoz, get_token: Callable[[str, str], str]
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user