Compare commits

...

11 Commits

Author SHA1 Message Date
aks07
cee4a3e2e2 feat: panel markers with dummy data 2025-10-28 19:03:52 +05:30
aks07
c0336001f5 Merge branch 'main' of github.com:SigNoz/signoz into feat/deployment-markers 2025-10-24 18:03:11 +05:30
SagarRajput-7
d05d394f57 chore: update slow running test in tracesExplorer test (#9396) 2025-10-23 11:02:02 +05:30
Vikrant Gupta
b4e5085a5a fix(sqlschema): postgres sqlschema get table operation (#9395)
* fix(sqlschema): postgres sqlschema get table operation

* fix(sqlschema): postgres sqlschema get table operation
2025-10-22 19:02:15 +05:30
Abhi kumar
88f7502a15 fix: prevent memory leaks from uncleaned uPlot event listeners (#9320) 2025-10-22 07:19:11 +00:00
primus-bot[bot]
b0442761ac chore(release): bump to v0.98.0 (#9393)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-10-22 12:09:31 +05:30
Vikrant Gupta
d539ca9bab feat(sql): swap mattn/sqlite with modernc.org/sqlite (#9343)
* feat(sql): swap mattn/sqlite with modernc.org/sqlite (#9325)

* feat(sql): swap mattn/sqlite with modernc.org/sqlite

* feat(sql): revert the dashboard testing changes

* feat(sql): enable WAL mode for sqlite

* feat(sql): revert enable WAL mode for sqlite

* feat(sql): use sensible defaults for busy_timeout

* feat(sql): add ldflags

* feat(sql): enable WAL mode for sqlite

* feat(sql): some fixes

* feat(sql): some fixes

* feat(sql): fix yarn lock and config defaults

* feat(sql): update the defaults in example.conf

* feat(sql): remove wal mode from integration tests
2025-10-21 18:45:48 +05:30
Vikrant Gupta
c8194e9abb fix(tokenizer): update the authn domains tooltips (#9388) 2025-10-21 11:25:44 +00:00
Yunus M
c919102fee chore: update on headers package version (#9376) 2025-10-20 17:04:37 +05:30
aks07
141380f1c7 feat: add tooltip to markers 2025-10-17 18:00:17 +05:30
aks07
7ba6a56115 feat: vertical markers plugin init 2025-10-17 02:26:52 +05:30
44 changed files with 1171 additions and 166 deletions

View File

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

View File

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

View File

@@ -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 }}"}'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,6 +27,7 @@ import { IUser } from 'providers/App/types';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import { KBarCommandPaletteProvider } from 'providers/KBarCommandPaletteProvider';
import { MarkersProvider } from 'providers/Markers/Markers';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useCallback, useEffect, useState } from 'react';
@@ -379,30 +380,34 @@ function App(): JSX.Element {
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<PreferenceContextProvider>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</PreferenceContextProvider>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
<MarkersProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<PreferenceContextProvider>
<Suspense
fallback={<Spinner size="large" tip="Loading..." />}
>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</PreferenceContextProvider>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</MarkersProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>

View File

@@ -0,0 +1,58 @@
.panel-markers-control {
padding: 16px;
.panel-markers-view-controller {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.markers-control-skeleton {
display: flex;
align-items: center;
gap: 24px;
margin-top: 8px;
.ant-skeleton-input{
width: 230px;
}
}
.panel-markers-inputs-section {
display: flex;
align-items: center;
gap: 24px;
margin-top: 8px;
.panel-markers-select-container {
display: flex;
align-items: center;
margin-bottom: 8px;
.custom-multiselect-wrapper {
width: fit-content;
min-width: 230px;
}
}
}
}
.variable-name {
display: flex;
min-width: 56px;
height: 32px;
padding: 6px 6px 6px 8px;
align-items: center;
gap: 4px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
color: var(--bg-robin-300);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
.info-icon {
margin-left: 4px;
color: var(--bg-vanilla-400);
}
}

View File

@@ -0,0 +1,3 @@
export const MARKER_TYPES = {
DEPLOYMENT: 'deployment',
};

View File

@@ -0,0 +1,70 @@
import type { Dispatch } from 'react';
import { useReducer } from 'react';
import type { MarkerControlState, MarkerQueryState } from '../types';
export const MARKER_ACTIONS = {
TOGGLE_SHOW_MARKERS: 'toggleShowMarkers',
SET_MARKER_SERVICES: 'setMarkerServices',
SET_MARKER_TYPES: 'setMarkerTypes',
SET_DEFAULTS_ON: 'setDefaultsOn',
RESET: 'reset',
} as const;
export type MarkerActionType = typeof MARKER_ACTIONS[keyof typeof MARKER_ACTIONS];
export type MarkerControlAction =
| { type: typeof MARKER_ACTIONS.TOGGLE_SHOW_MARKERS; payload: boolean }
| { type: typeof MARKER_ACTIONS.SET_MARKER_SERVICES; payload: string[] }
| { type: typeof MARKER_ACTIONS.SET_MARKER_TYPES; payload: string[] }
| {
type: typeof MARKER_ACTIONS.SET_DEFAULTS_ON;
payload: { markerServices: string[]; markerTypes: string[] };
}
| { type: typeof MARKER_ACTIONS.RESET };
function normalizeInitialState(
state: MarkerQueryState | null,
): MarkerControlState {
return {
showMarkers: state?.showMarkers ? 1 : 0,
markerServices: state?.markerServices || [],
markerTypes: state?.markerTypes || [],
};
}
function reducer(
state: MarkerControlState,
action: MarkerControlAction,
): MarkerControlState {
switch (action.type) {
case MARKER_ACTIONS.TOGGLE_SHOW_MARKERS:
return { ...state, showMarkers: action.payload ? 1 : 0 };
case MARKER_ACTIONS.SET_MARKER_SERVICES:
return { ...state, markerServices: action.payload };
case MARKER_ACTIONS.SET_MARKER_TYPES:
return { ...state, markerTypes: action.payload };
case MARKER_ACTIONS.SET_DEFAULTS_ON:
return {
...state,
showMarkers: 1,
markerServices: action.payload.markerServices,
markerTypes: action.payload.markerTypes,
};
case MARKER_ACTIONS.RESET:
return { showMarkers: 0, markerServices: [], markerTypes: [] };
default:
return state;
}
}
export default function useMarkerControlState(
initialQueryState: MarkerQueryState | null,
): {
store: MarkerControlState;
dispatch: Dispatch<MarkerControlAction>;
} {
const initial = normalizeInitialState(initialQueryState);
const [store, dispatch] = useReducer(reducer, initial);
return { store, dispatch };
}

View File

@@ -0,0 +1,57 @@
import {
clearLocalStorageState,
getLocalStorageState,
getMarkerStateFromQuery,
getQueryParamsFromState,
setLocalStorageState,
} from 'components/PanelMarkersControl/utils';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useEffect } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
type MarkerHandlers = {
onMarkerToggleOn: () => void;
onMarkerToggleOff: () => void;
};
const useMarkerHandlers = ({ key }: { key: string }): MarkerHandlers => {
const urlQuery = useUrlQuery();
const { search } = useLocation();
const history = useHistory();
// useEffect to sync url query with local storage
useEffect(() => {
const queryState = getMarkerStateFromQuery(urlQuery);
const localStorageState = getLocalStorageState(key);
if (queryState === null && localStorageState?.showMarkers) {
const params = new URLSearchParams(search);
const queryParams = getQueryParamsFromState(params, localStorageState);
history.replace({ search: queryParams.toString() });
} else {
setLocalStorageState(key, queryState);
}
}, [urlQuery, key, search, history]);
const onMarkerToggleOn = useCallback(() => {
// set defaults for service and marker type
const params = new URLSearchParams(search);
params.set('showMarkers', '1');
history.replace({ search: params.toString() });
}, [search, history]);
const onMarkerToggleOff = useCallback(() => {
// important to clear both url query and local storage here. Else url local storage sync useEffect will not work as expected.
clearLocalStorageState(key);
const params = new URLSearchParams(search);
params.delete('showMarkers');
params.delete('markerServices');
params.delete('markerTypes');
history.replace({ search: params.toString() });
}, [key, search, history]);
return { onMarkerToggleOn, onMarkerToggleOff };
};
export default useMarkerHandlers;

View File

@@ -0,0 +1,234 @@
import './PanelMarkersControl.scss';
import { Skeleton, Switch, Typography } from 'antd';
import CustomMultiSelect from 'components/NewSelect/CustomMultiSelect';
import { MARKER_TYPES } from 'components/PanelMarkersControl/constants';
import useMarkerControlState, {
MARKER_ACTIONS,
} from 'components/PanelMarkersControl/hooks/useMarkerControlState';
import useMarkerHandlers from 'components/PanelMarkersControl/hooks/useMarkerHandlers';
import {
getInitialStateForControls,
getQueryParamsFromState,
} from 'components/PanelMarkersControl/utils';
import useUrlQuery from 'hooks/useUrlQuery';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useFetchMarkersData, useMarkers } from 'providers/Markers/Markers';
import { useCallback, useEffect, useMemo } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
function PanelMarkersControl(): JSX.Element {
const urlQuery = useUrlQuery();
const { search } = useLocation();
const history = useHistory();
const { selectedDashboard } = useDashboard();
const { store: markerControlState, dispatch } = useMarkerControlState(
getInitialStateForControls(selectedDashboard?.id || '', urlQuery),
);
const { markersData, setMarkersData } = useMarkers();
const { loadingMarkers } = useFetchMarkersData({
isFetchEnabled: markerControlState.showMarkers === 1,
});
const { onMarkerToggleOn, onMarkerToggleOff } = useMarkerHandlers({
key: selectedDashboard?.id || '',
});
// API integration: check if this is correct
const markerTypeOptions = useMemo(() => {
const uniqueTypes = Array.from(
new Set((markersData || []).map((m: any) => m?.type).filter(Boolean)),
);
return uniqueTypes.map((t: string) => ({
label: t.charAt(0).toUpperCase() + t.slice(1),
value: t,
}));
}, [markersData]);
// API integration: check if this is correct
const serviceNameOptions = useMemo(() => {
const uniqueServices = Array.from(
new Set(
(markersData || [])
.map((m: any) => m?.attr?.['service.name'])
.filter(Boolean),
),
);
return uniqueServices.map((s: string) => ({ label: s, value: s }));
}, [markersData]);
const handleServiceChange = useCallback(
(serviceOrServices: string | string[] | undefined): void => {
let servicesArray: string[] = [];
if (Array.isArray(serviceOrServices)) {
servicesArray = serviceOrServices;
} else if (serviceOrServices) {
servicesArray = [serviceOrServices];
}
dispatch({
type: MARKER_ACTIONS.SET_MARKER_SERVICES,
payload: servicesArray,
});
// sync URL param
const params = new URLSearchParams(search);
if (servicesArray.length > 0) {
params.set('markerServices', servicesArray.join(','));
} else {
params.delete('markerServices');
}
history.replace({ search: params.toString() });
},
[history, search, dispatch],
);
const handleMarkerTypesChange = useCallback(
(typesOrArray: string | string[] | undefined): void => {
let typesArray: string[] = [];
if (Array.isArray(typesOrArray)) {
typesArray = typesOrArray;
} else if (typesOrArray) {
typesArray = [typesOrArray];
}
dispatch({ type: MARKER_ACTIONS.SET_MARKER_TYPES, payload: typesArray });
const params = new URLSearchParams(search);
if (typesArray.length > 0) {
params.set('markerTypes', typesArray.join(','));
} else {
params.delete('markerTypes');
}
history.replace({ search: params.toString() });
},
[history, search, dispatch],
);
const handleToggleShowMarkers = useCallback(
(checked: boolean): void => {
dispatch({ type: MARKER_ACTIONS.TOGGLE_SHOW_MARKERS, payload: checked });
if (checked) {
// get default services and marker types from markersData
onMarkerToggleOn();
} else {
// consider using useReducer to reset the state
setMarkersData([]);
dispatch({ type: MARKER_ACTIONS.RESET });
onMarkerToggleOff();
}
},
[onMarkerToggleOn, onMarkerToggleOff, dispatch, setMarkersData],
);
useEffect(() => {
if (markersData.length < 1) return;
/**
* On markers data change, derive defaults (or use query selections if present)
* and set them in a single reducer dispatch.
*/
const queryMarkerServicesRaw = urlQuery.get('markerServices') || '';
const queryMarkerTypesRaw = urlQuery.get('markerTypes') || '';
const defMarkerServices = ['cart-service'];
const defMarkerTypes = [MARKER_TYPES.DEPLOYMENT];
const servicesArray = queryMarkerServicesRaw
? queryMarkerServicesRaw
.split(',')
.map((s) => s.trim())
.filter((s) => s.length > 0)
: defMarkerServices;
const typesArray = queryMarkerTypesRaw
? queryMarkerTypesRaw
.split(',')
.map((t) => t.trim())
.filter((t) => t.length > 0)
: defMarkerTypes;
dispatch({
type: MARKER_ACTIONS.SET_DEFAULTS_ON,
payload: { markerServices: servicesArray, markerTypes: typesArray },
});
// reflect in URL params as well
const params = new URLSearchParams(search);
const queryParams = getQueryParamsFromState(params, {
showMarkers: 1,
markerServices: servicesArray,
markerTypes: typesArray,
});
history.replace({ search: queryParams.toString() });
console.log('>>> markersData', markersData);
// urlQuery removed from dependencies as not able to unset markerTypes. But works with markerServices. [CHECK THIS]
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [markersData]);
// ADD INITIAL STATE FOR SELECTED SERVICES AND MARKER TYPES
return (
<div className="panel-markers-control">
<div className="panel-markers-view-controller">
<Typography>Show Markers</Typography>
<Switch
size="small"
checked={markerControlState.showMarkers === 1}
onChange={handleToggleShowMarkers}
/>
</div>
{markerControlState.showMarkers === 1 && (
<div className="panel-markers-inputs-section">
{loadingMarkers ? (
<div className="markers-control-skeleton">
<Skeleton.Input active size="small" />
<Skeleton.Input active size="small" />
</div>
) : (
<>
<div className="panel-markers-select-container">
<Typography.Text className="variable-name" ellipsis>
Marker type
</Typography.Text>
<CustomMultiSelect
className="panel-markers-select"
placeholder="Select one or more marker types"
enableAllSelection={false}
options={markerTypeOptions}
maxTagCount={3}
value={markerControlState.markerTypes}
onChange={handleMarkerTypesChange}
/>
</div>
<div className="panel-markers-select-container">
<Typography.Text className="variable-name" ellipsis>
Service name
</Typography.Text>
<CustomMultiSelect
className="panel-markers-select"
placeholder="Select one or more service names"
maxTagCount={3}
enableAllSelection={false}
options={serviceNameOptions}
value={markerControlState.markerServices}
onChange={handleServiceChange}
/>
</div>
</>
)}
</div>
)}
</div>
);
}
// select bright color for the markers
// convert panel marker state to useReducer
// removed urlQuery from dependencies.
// filters on markersData should work properly. If no markers selected. Show no markers.
export default PanelMarkersControl;

View File

@@ -0,0 +1,7 @@
export interface MarkerControlState {
showMarkers: number;
markerServices: string[];
markerTypes: string[];
}
export type MarkerQueryState = MarkerControlState | null;

View File

@@ -0,0 +1,108 @@
import type { MarkerQueryState } from 'components/PanelMarkersControl/types';
import { LOCALSTORAGE } from 'constants/localStorage';
// CHECK LOGIC AND CREATE UTIL
export function getMarkerStateFromQuery(
urlQuery: URLSearchParams,
): MarkerQueryState | null {
const showMarkers = urlQuery.get('showMarkers') === '1';
const servicesRaw = urlQuery.get('markerServices') || '';
if (!showMarkers) {
return null;
}
const markerTypesParam = urlQuery.get('markerTypes') || '';
const markerTypes = markerTypesParam
.split(',')
.map((t) => t.trim())
.filter((t) => t.length > 0);
const markerServices = servicesRaw
.split(',')
.map((s) => s.trim())
.filter((s) => s.length > 0);
return {
showMarkers: 1,
markerServices,
markerTypes,
};
}
export const getLocalStorageState = (key: string): MarkerQueryState | null => {
const raw = localStorage.getItem(LOCALSTORAGE.MARKERS_OVERLAY_STATE);
try {
const parsed = raw ? JSON.parse(raw) : null;
return parsed?.[key] ?? null;
} catch (_) {
return null;
}
};
export const getQueryParamsFromState = (
params: URLSearchParams,
state: MarkerQueryState,
): URLSearchParams => {
if (!state) {
return params;
}
if (state.showMarkers) {
params.set('showMarkers', String(state.showMarkers) || '0');
}
if (Array.isArray(state.markerServices) && state.markerServices.length > 0) {
params.set('markerServices', state.markerServices.join(','));
}
if (Array.isArray(state.markerTypes) && state.markerTypes.length > 0) {
params.set('markerTypes', state.markerTypes.join(','));
}
return params;
};
export const setLocalStorageState = (
key: string,
state: MarkerQueryState | null,
): void => {
if (!key || key.trim().length === 0) {
return;
}
try {
const raw = localStorage.getItem(LOCALSTORAGE.MARKERS_OVERLAY_STATE);
let obj: Record<string, unknown> = {};
try {
obj = raw ? JSON.parse(raw) : {};
} catch (_) {
obj = {};
}
obj[key] = state;
localStorage.setItem(LOCALSTORAGE.MARKERS_OVERLAY_STATE, JSON.stringify(obj));
} catch (_) {
// ignore storage errors
}
};
export const getInitialStateForControls = (
key: string,
urlQuery: URLSearchParams,
): MarkerQueryState | null => {
const queryState = getMarkerStateFromQuery(urlQuery);
const localStorageState = getLocalStorageState(key);
return queryState ?? localStorageState;
};
export const clearLocalStorageState = (key: string): void => {
if (!key || key.trim().length === 0) {
return;
}
const raw = localStorage.getItem(LOCALSTORAGE.MARKERS_OVERLAY_STATE);
let obj: Record<string, unknown> = {};
try {
obj = raw ? JSON.parse(raw) : {};
} catch (_) {
obj = {};
}
delete obj[key];
localStorage.setItem(LOCALSTORAGE.MARKERS_OVERLAY_STATE, JSON.stringify(obj));
};

View File

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

View File

@@ -0,0 +1,177 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable sonarjs/no-duplicate-string */
import uPlot from 'uplot';
type MarkersData = { id: string | number; val: number; stroke?: string };
export function verticalMarkersPlugin({
markersData = [],
lineType = [5, 3],
width = 1,
}: {
markersData?: MarkersData[];
lineType?: number[];
width?: number;
} = {}): uPlot.Plugin {
const DEFAULT_STROKE = 'rgba(0, 102, 255, 0.95)';
let removeListeners: (() => void) | null = null;
let tooltipEl: HTMLDivElement | null = null;
const renderAxisMarkers = (uu: uPlot): void => {
const axes = uu.root.querySelectorAll('.u-axis');
const xAxis = (axes && (axes[0] as HTMLElement)) || null;
if (!xAxis) return;
// attach delegated hover/mouseout listeners once on the x-axis container
if (!(xAxis as HTMLElement).dataset?.vlineHoverAttached) {
const onMouseOver = (e: MouseEvent): void => {
const target = e.target as HTMLElement;
if (!target?.classList?.contains('vline-triangle-marker')) return;
const { id } = target.dataset;
const valStr = target.dataset.val;
const val = valStr ? Number(valStr) : undefined;
// const mData = markersData.find((d) => String(d.id) === String(id));
// create tooltip
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.className = 'vline-marker-tooltip';
Object.assign(tooltipEl.style, {
position: 'fixed',
padding: '6px 8px',
borderRadius: '4px',
fontSize: '12px',
zIndex: '10000',
pointerEvents: 'none',
background: '#111827',
color: '#e5e7eb',
border: '1px solid #374151',
});
document.body.appendChild(tooltipEl);
}
tooltipEl.textContent = `id: ${id ?? ''} • ts: ${val ?? ''}`;
// position near cursor
tooltipEl.style.left = `${e.clientX + 10}px`;
tooltipEl.style.top = `${e.clientY - 28}px`;
};
const onMouseOut = (e: MouseEvent): void => {
const target = e.target as HTMLElement;
if (!target?.classList?.contains('vline-triangle-marker')) return;
if (tooltipEl) {
tooltipEl.remove();
tooltipEl = null;
}
};
xAxis.addEventListener('mouseover', onMouseOver);
xAxis.addEventListener('mouseout', onMouseOut);
removeListeners = (): void => {
xAxis.removeEventListener('mouseover', onMouseOver);
xAxis.removeEventListener('mouseout', onMouseOut);
};
(xAxis as HTMLElement).dataset.vlineHoverAttached = '1';
}
// cleanup markers to avoid duplicates on rerender/resize
xAxis.querySelectorAll('.vline-triangle-marker').forEach((el) => el.remove());
const plotLeft = uu.bbox.left;
const plotRight = plotLeft + uu.bbox.width;
for (let i = 0; i < markersData.length; i++) {
const mData = markersData[i];
const xAbs = uu.valToPos(mData.val, 'x', true);
if (xAbs >= plotLeft && xAbs <= plotRight) {
const xPx = (xAbs - plotLeft) / window.devicePixelRatio;
const marker = document.createElement('div');
marker.className = 'vline-triangle-marker';
marker.dataset.id = String(mData.id); // may change later after BE discussion
marker.dataset.val = String(mData.val); // TODO: remove this later
Object.assign(marker.style, {
position: 'absolute',
width: '0px',
height: '0px',
borderLeft: '5px solid transparent',
borderRight: '5px solid transparent',
borderBottomWidth: '5px',
borderBottomStyle: 'solid',
borderBottomColor: mData.stroke || DEFAULT_STROKE,
transform: 'translateX(-50%)',
cursor: 'pointer',
zIndex: '1',
left: `${xPx}px`,
});
xAxis.appendChild(marker);
}
}
};
return {
hooks: {
destroy: [
(): void => {
if (tooltipEl) {
tooltipEl.remove();
tooltipEl = null;
}
if (removeListeners) {
removeListeners();
removeListeners = null;
}
},
],
drawAxes: [
(uu: uPlot): void => {
renderAxisMarkers(uu);
},
],
draw: [
(uu: uPlot): void => {
const { ctx } = uu;
const { top } = uu.bbox;
const bottom = top + uu.bbox.height;
const plotLeft = uu.bbox.left;
const plotRight = plotLeft + uu.bbox.width;
ctx.save();
for (let i = 0; i < markersData.length; i++) {
const mData = markersData[i];
const x = uu.valToPos(mData.val, 'x', true);
// only draw if within plot bounds
if (x >= plotLeft && x <= plotRight) {
ctx.beginPath();
ctx.strokeStyle = mData.stroke || DEFAULT_STROKE;
ctx.lineWidth = width;
ctx.setLineDash(lineType || []);
ctx.moveTo(x, top);
ctx.lineTo(x, bottom);
ctx.stroke();
}
}
ctx.restore();
},
],
},
};
}
// MOVE TO REACT. or use portal for tooltip.
// correct the format should work with expected data from BE
// Remove cognitive complexity rule.
// logic to get marker plugin to be added to context
// depending on type of marker parse the data to choose color. example deployment should be red etc.
// pass such data with multple colors to check render.
// PERF CHECK.
// drive data passing from the context for markers. data in this is enriched only when we
// use <ShowMarkers /> component. else it will be empty.
// plugins. and pass marker hooks using this
// should only pass marker hook if data is present(shouldRenderMarker memo)
// depending on type of marker parse the data to choose color. example deployment should be red etc.
// pass such data with multple colors to check render.
// PERF CHECK.
// drive data passing from the context for markers. data in this is enriched only when we
// use <ShowMarkers /> component. else it will be empty.

View File

@@ -36,4 +36,5 @@ export enum LOCALSTORAGE {
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',
SHOW_FREQUENCY_CHART = 'SHOW_FREQUENCY_CHART',
DISSMISSED_COST_METER_INFO = 'DISMISSED_COST_METER_INFO',
MARKERS_OVERLAY_STATE = 'MARKERS_OVERLAY_STATE',
}

View File

@@ -13,6 +13,7 @@ import {
} from 'antd';
import logEvent from 'api/common/logEvent';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import PanelMarkersControl from 'components/PanelMarkersControl';
import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
@@ -488,6 +489,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
<DashboardVariableSelection />
</section>
)}
<PanelMarkersControl />
<DashboardGraphSlider />
<Modal

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
/* eslint-disable sonarjs/no-duplicate-string */
import './UplotPanelWrapper.styles.scss';
import { Alert } from 'antd';
import { ToggleGraphProps } from 'components/Graph/types';
import Uplot from 'components/Uplot';
import { verticalMarkersPlugin } from 'components/Uplot/plugins/verticalMarkersPlugin';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GraphManager from 'container/GridCardLayout/GridCard/FullView/GraphManager';
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
@@ -17,6 +20,7 @@ import { cloneDeep, isEqual, isUndefined } from 'lodash-es';
import _noop from 'lodash-es/noop';
import { ContextMenu, useCoordinates } from 'periscope/components/ContextMenu';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useMarkers } from 'providers/Markers/Markers';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { DataSource } from 'types/common/queryBuilder';
@@ -55,6 +59,32 @@ function UplotPanelWrapper({
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const { currentQuery } = useQueryBuilder();
const { filteredMarkersData, shouldShowMarkers } = useMarkers();
const markersPlugin: uPlot.Plugin | null = useMemo(() => {
if (shouldShowMarkers) {
console.log('*** filteredMarkersData', filteredMarkersData);
console.log('*** shouldShowMarkers', {
markersData: [
{ id: 'm1', val: 1760625000, stroke: 'rgba(96, 255, 128, 0.95)' },
{ id: 'm2', val: 1760630000, stroke: 'rgba(255, 96, 96, 0.95)' },
{ id: 'm3', val: 1760640000, stroke: 'rgba(255, 96, 96, 0.95)' },
],
lineType: [6, 4],
width: 1,
});
return verticalMarkersPlugin({
markersData: [
{ id: 'm1', val: 1760625000, stroke: 'rgba(96, 255, 128, 0.95)' },
{ id: 'm2', val: 1760630000, stroke: 'rgba(255, 96, 96, 0.95)' },
{ id: 'm3', val: 1760640000, stroke: 'rgba(255, 96, 96, 0.95)' },
],
lineType: [6, 4],
width: 1,
});
}
return null;
}, [shouldShowMarkers, filteredMarkersData]);
const [hiddenGraph, setHiddenGraph] = useState<{ [key: string]: boolean }>();
@@ -249,6 +279,7 @@ function UplotPanelWrapper({
}) => {
legendScrollPositionRef.current = position;
},
customPlugins: [...(markersPlugin ? [markersPlugin] : [])],
}),
[
queryResponse.data?.payload,
@@ -270,6 +301,7 @@ function UplotPanelWrapper({
onClickHandler,
widget,
stackedBarChart,
markersPlugin,
],
);

View File

@@ -36,6 +36,7 @@ import { getYAxisScale } from './utils/getYAxisScale';
interface ExtendedUPlot extends uPlot {
_legendScrollCleanup?: () => void;
_tooltipCleanup?: () => void;
_legendElementCleanup?: Array<() => void>;
}
export interface GetUPlotChartOptions {
@@ -86,6 +87,7 @@ export interface GetUPlotChartOptions {
scrollTop: number;
scrollLeft: number;
}) => void;
customPlugins?: uPlot.Plugin[];
}
/** the function converts series A , series B , series C to
@@ -217,6 +219,7 @@ export const getUPlotChartOptions = ({
query,
legendScrollPosition,
setLegendScrollPosition,
customPlugins = [],
}: GetUPlotChartOptions): uPlot.Options => {
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
@@ -386,6 +389,7 @@ export const getUPlotChartOptions = ({
],
},
},
...customPlugins,
],
hooks: {
draw: [
@@ -473,6 +477,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 +646,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 +676,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 +698,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 +741,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 +755,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 || [])

View File

@@ -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'));

View File

@@ -0,0 +1,184 @@
import useUrlQuery from 'hooks/useUrlQuery';
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
type Marker = {
type: string;
env: string;
source: string;
attr: {
version: string;
firstSeen: number;
'service.name': string;
region: string;
spanCount: number;
};
};
function generateMockMarkers(
minTime: number,
maxTime: number,
count = 10,
): { markers: Marker[] } {
const step = (maxTime - minTime) / count;
const services = [
'cart-service',
'checkout-service',
'inventory-service',
'auth-service',
'payment-service',
'recommendation-service',
'search-service',
'user-service',
'notification-service',
'analytics-service',
];
const regions = [
'us-east-1',
'us-west-1',
'us-east-2',
'eu-central-1',
'ap-southeast-1',
'eu-west-1',
];
const envs = ['prod-us', 'prod-eu', 'staging', 'prod-ap'];
const sources = ['traces', 'logs'];
const markers: Marker[] = Array.from({ length: count }).map((_, i) => {
const firstSeen = Math.round(minTime + i * step);
const service = services[i % services.length];
const env = envs[i % envs.length];
const region = regions[i % regions.length];
const source = sources[i % sources.length];
return {
type: 'deployment',
env,
source,
attr: {
version: `1.${160 + i}.0-${env.includes('prod') ? 'prod' : 'stg'}`,
firstSeen,
'service.name': service,
region,
spanCount: 100 + Math.floor(Math.random() * 150),
},
};
});
return { markers };
}
type MarkersContextValue = {
markersData: Marker[];
filteredMarkersData: Marker[];
setMarkersData: (markersData: Marker[]) => void;
shouldShowMarkers: boolean;
};
const MarkersContext = createContext<MarkersContextValue | undefined>(
undefined,
);
export function MarkersProvider({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
const [markersData, setMarkersData] = useState<Marker[]>([]);
const urlQuery = useUrlQuery();
// CHECK LOGIC AND CREATE UTIL
const filteredMarkersData = useMemo(() => {
const servicesRaw = urlQuery.get('markerServices') || '';
const typesRaw = urlQuery.get('markerTypes') || '';
const selectedServices = servicesRaw
.split(',')
.map((s) => s.trim())
.filter((s) => s.length > 0);
const selectedTypes = typesRaw
.split(',')
.map((t) => t.trim())
.filter((t) => t.length > 0);
if (selectedServices.length === 0 && selectedTypes.length === 0) {
return markersData;
}
return (markersData || []).filter((m: Marker) => {
const typeOk = selectedTypes.includes(m?.type);
const serviceOk = selectedServices.includes(m?.attr?.['service.name']);
return typeOk && serviceOk;
});
}, [urlQuery, markersData]);
const shouldShowMarkers = useMemo(() => !!urlQuery.get('showMarkers'), [
urlQuery,
]);
console.log('*** filteredMarkersData', filteredMarkersData);
const value = useMemo(
() => ({
markersData,
filteredMarkersData,
setMarkersData,
shouldShowMarkers,
}),
[markersData, filteredMarkersData, setMarkersData, shouldShowMarkers],
);
return (
<MarkersContext.Provider value={value}>{children}</MarkersContext.Provider>
);
}
export function useMarkers(): MarkersContextValue {
const ctx = useContext(MarkersContext);
if (!ctx) {
throw new Error('useMarkers must be used within a MarkersProvider');
}
return ctx;
}
export function useFetchMarkersData({
isFetchEnabled,
}: {
isFetchEnabled: boolean;
}): { loadingMarkers: boolean } {
const { setMarkersData } = useMarkers();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { data, isLoading, isFetching } = useQuery<{ markers: Marker[] }>(
['mock-markers', minTime, maxTime],
async () => {
// simulate network latency without returning from the executor
await new Promise<void>((resolve) => {
setTimeout(resolve, 2000);
});
return generateMockMarkers(minTime, maxTime, 10);
},
{
enabled: isFetchEnabled,
},
);
useEffect(() => {
if (data) {
console.log('*** setting markers data', data.markers);
setMarkersData(data.markers);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
return {
loadingMarkers: isLoading || isFetching,
};
}
export default MarkersProvider;

View File

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

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

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

View File

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

View File

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

View File

@@ -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...)
}
}

View File

@@ -15,8 +15,6 @@ builds:
- id: signoz
binary: bin/histogram-quantile
main: scripts/clickhouse/histogramquantile/main.go
env:
- CGO_ENABLED=0
goos:
- linux
- darwin

View File

@@ -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",
)

View File

@@ -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"],
)

View File

@@ -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"},

View File

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