mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-11 12:04:27 +00:00
Compare commits
17 Commits
forgot-pas
...
test/uplot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2578ef4476 | ||
|
|
993804aa1b | ||
|
|
0680b71608 | ||
|
|
82c4dd5e9b | ||
|
|
568a0fd121 | ||
|
|
d48ec38f4f | ||
|
|
8285cc3fee | ||
|
|
de2b4adcc1 | ||
|
|
c7bc2a05e3 | ||
|
|
f88c020a6c | ||
|
|
56e630ce8d | ||
|
|
8f4bc50b9c | ||
|
|
ff336719b9 | ||
|
|
9b02b3daf3 | ||
|
|
87b8855bfe | ||
|
|
9f8e655fcd | ||
|
|
cba9c820a4 |
@@ -42,7 +42,7 @@ services:
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
schema-migrator-sync:
|
||||
image: signoz/signoz-schema-migrator:v0.142.0
|
||||
image: signoz/signoz-schema-migrator:v0.129.13
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
@@ -55,7 +55,7 @@ services:
|
||||
condition: service_healthy
|
||||
restart: on-failure
|
||||
schema-migrator-async:
|
||||
image: signoz/signoz-schema-migrator:v0.142.0
|
||||
image: signoz/signoz-schema-migrator:v0.129.13
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
10
.github/workflows/goci.yaml
vendored
10
.github/workflows/goci.yaml
vendored
@@ -93,13 +93,3 @@ jobs:
|
||||
run: |
|
||||
go run cmd/enterprise/*.go generate openapi
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in openapi spec. Run go run cmd/enterprise/*.go generate openapi locally and commit."; exit 1)
|
||||
- name: node-install
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: install-frontend
|
||||
run: cd frontend && yarn install
|
||||
- name: generate-api-clients
|
||||
run: |
|
||||
cd frontend && yarn generate:api
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated api clients. Run yarn generate:api in frontend/ locally and commit."; exit 1)
|
||||
|
||||
@@ -176,7 +176,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.111.0
|
||||
image: signoz/signoz:v0.110.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
@@ -209,7 +209,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.142.0
|
||||
image: signoz/signoz-otel-collector:v0.129.13
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
@@ -233,7 +233,7 @@ services:
|
||||
- signoz
|
||||
schema-migrator:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:v0.142.0
|
||||
image: signoz/signoz-schema-migrator:v0.129.13
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.111.0
|
||||
image: signoz/signoz:v0.110.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
@@ -150,7 +150,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.142.0
|
||||
image: signoz/signoz-otel-collector:v0.129.13
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
@@ -176,7 +176,7 @@ services:
|
||||
- signoz
|
||||
schema-migrator:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:v0.142.0
|
||||
image: signoz/signoz-schema-migrator:v0.129.13
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -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.111.0}
|
||||
image: signoz/signoz:${VERSION:-v0.110.1}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
@@ -213,7 +213,7 @@ services:
|
||||
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.13}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
@@ -239,7 +239,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.142.0}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.13}
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
@@ -250,7 +250,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-async:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.142.0}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.13}
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
@@ -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.111.0}
|
||||
image: signoz/signoz:${VERSION:-v0.110.1}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
@@ -144,7 +144,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.13}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
@@ -166,7 +166,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.142.0}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.13}
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
@@ -178,7 +178,7 @@ services:
|
||||
restart: on-failure
|
||||
schema-migrator-async:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.142.0}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.13}
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
@@ -4355,8 +4355,6 @@ components:
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
Querybuildertypesv5QueryData:
|
||||
properties:
|
||||
@@ -4429,9 +4427,6 @@ components:
|
||||
type: array
|
||||
nullable: true
|
||||
type: object
|
||||
required:
|
||||
- keys
|
||||
- complete
|
||||
type: object
|
||||
TelemetrytypesGettableFieldValues:
|
||||
properties:
|
||||
@@ -4439,9 +4434,6 @@ components:
|
||||
type: boolean
|
||||
values:
|
||||
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldValues'
|
||||
required:
|
||||
- values
|
||||
- complete
|
||||
type: object
|
||||
TelemetrytypesTelemetryFieldKey:
|
||||
properties:
|
||||
@@ -4457,8 +4449,6 @@ components:
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
TelemetrytypesTelemetryFieldValues:
|
||||
properties:
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface MockUPlotInstance {
|
||||
export interface MockUPlotPaths {
|
||||
spline: jest.Mock;
|
||||
bars: jest.Mock;
|
||||
linear: jest.Mock;
|
||||
stepped: jest.Mock;
|
||||
}
|
||||
|
||||
// Create mock instance methods
|
||||
@@ -23,10 +25,23 @@ const createMockUPlotInstance = (): MockUPlotInstance => ({
|
||||
setSeries: jest.fn(),
|
||||
});
|
||||
|
||||
// Create mock paths
|
||||
const mockPaths: MockUPlotPaths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
// Path builder: (self, seriesIdx, idx0, idx1) => paths or null
|
||||
const createMockPathBuilder = (name: string): jest.Mock =>
|
||||
jest.fn(() => ({
|
||||
name, // To test if the correct pathBuilder is used
|
||||
stroke: jest.fn(),
|
||||
fill: jest.fn(),
|
||||
clip: jest.fn(),
|
||||
}));
|
||||
|
||||
// Create mock paths - linear, spline, stepped needed by UPlotSeriesBuilder.getPathBuilder
|
||||
const mockPaths = {
|
||||
spline: jest.fn(() => createMockPathBuilder('spline')),
|
||||
bars: jest.fn(() => createMockPathBuilder('bars')),
|
||||
linear: jest.fn(() => createMockPathBuilder('linear')),
|
||||
stepped: jest.fn((opts?: { align?: number }) =>
|
||||
createMockPathBuilder(`stepped-(${opts?.align ?? 0})`),
|
||||
),
|
||||
};
|
||||
|
||||
// Mock static methods
|
||||
|
||||
@@ -17,8 +17,6 @@ const config: Config.InitialOptions = {
|
||||
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||
'^@signozhq/icons$':
|
||||
'<rootDir>/node_modules/@signozhq/icons/dist/index.esm.js',
|
||||
},
|
||||
globals: {
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"commitlint": "commitlint --edit $1",
|
||||
"test": "jest",
|
||||
"test:changedsince": "jest --changedSince=main --coverage --silent",
|
||||
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh"
|
||||
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh && prettier --write src/api/generated && (eslint --fix src/api/generated || true)"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.15.0"
|
||||
@@ -52,7 +52,6 @@
|
||||
"@signozhq/combobox": "0.0.2",
|
||||
"@signozhq/command": "0.0.0",
|
||||
"@signozhq/design-tokens": "2.1.1",
|
||||
"@signozhq/icons": "0.1.0",
|
||||
"@signozhq/input": "0.0.2",
|
||||
"@signozhq/popover": "0.0.0",
|
||||
"@signozhq/resizable": "0.0.0",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="184" height="188" fill="none" viewBox="0 0 184 188"><path fill="#f3b01c" d="M108.092 130.021c18.166-2.018 35.293-11.698 44.723-27.854-4.466 39.961-48.162 65.218-83.83 49.711-3.286-1.425-6.115-3.796-8.056-6.844-8.016-12.586-10.65-28.601-6.865-43.135 10.817 18.668 32.81 30.111 54.028 28.122"/><path fill="#8d2676" d="M53.401 90.174c-7.364 17.017-7.682 36.94 1.345 53.336-31.77-23.902-31.423-75.052-.388-98.715 2.87-2.187 6.282-3.485 9.86-3.683 14.713-.776 29.662 4.91 40.146 15.507-21.3.212-42.046 13.857-50.963 33.555"/><path fill="#ee342f" d="M114.637 61.855C103.89 46.87 87.069 36.668 68.639 36.358c35.625-16.17 79.446 10.047 84.217 48.807.444 3.598-.139 7.267-1.734 10.512-6.656 13.518-18.998 24.002-33.42 27.882 10.567-19.599 9.263-43.544-3.065-61.704"/></svg>
|
||||
|
Before Width: | Height: | Size: 811 B |
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"SIGN_UP": "SigNoz | Sign Up",
|
||||
"LOGIN": "SigNoz | Login",
|
||||
"FORGOT_PASSWORD": "SigNoz | Forgot Password",
|
||||
"HOME": "SigNoz | Home",
|
||||
"SERVICE_METRICS": "SigNoz | Service Metrics",
|
||||
"SERVICE_MAP": "SigNoz | Service Map",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "\n\n---\nRenamed tag files to index.ts...\n"
|
||||
# Rename tag files to index.ts in services directories
|
||||
# tags-split creates: services/tagName/tagName.ts -> rename to services/tagName/index.ts
|
||||
find src/api/generated/services -mindepth 1 -maxdepth 1 -type d | while read -r dir; do
|
||||
@@ -12,33 +11,4 @@ find src/api/generated/services -mindepth 1 -maxdepth 1 -type d | while read -r
|
||||
fi
|
||||
done
|
||||
|
||||
echo "\n✅ Tag files renamed to index.ts"
|
||||
|
||||
# Format generated files
|
||||
echo "\n\n---\nRunning prettier...\n"
|
||||
if ! prettier --write src/api/generated; then
|
||||
echo "Prettier formatting failed!"
|
||||
exit 1
|
||||
fi
|
||||
echo "\n✅ Prettier formatting successful"
|
||||
|
||||
|
||||
# Fix linting issues
|
||||
echo "\n\n---\nRunning eslint...\n"
|
||||
if ! yarn lint --fix --quiet src/api/generated; then
|
||||
echo "ESLint check failed! Please fix linting errors before proceeding."
|
||||
exit 1
|
||||
fi
|
||||
echo "\n✅ ESLint check successful"
|
||||
|
||||
|
||||
# Check for type errors
|
||||
echo "\n\n---\nChecking for type errors...\n"
|
||||
if ! tsc --noEmit; then
|
||||
echo "Type check failed! Please fix type errors before proceeding."
|
||||
exit 1
|
||||
fi
|
||||
echo "\n✅ Type check successful"
|
||||
|
||||
|
||||
echo "\n\n---\n ✅✅✅ API generation complete!"
|
||||
echo "Tag files renamed to index.ts"
|
||||
|
||||
@@ -194,10 +194,6 @@ export const Login = Loadable(
|
||||
() => import(/* webpackChunkName: "Login" */ 'pages/Login'),
|
||||
);
|
||||
|
||||
export const ForgotPassword = Loadable(
|
||||
() => import(/* webpackChunkName: "ForgotPassword" */ 'pages/ForgotPassword'),
|
||||
);
|
||||
|
||||
export const UnAuthorized = Loadable(
|
||||
() => import(/* webpackChunkName: "UnAuthorized" */ 'pages/UnAuthorized'),
|
||||
);
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
DashboardWidget,
|
||||
EditRulesPage,
|
||||
ErrorDetails,
|
||||
ForgotPassword,
|
||||
Home,
|
||||
InfrastructureMonitoring,
|
||||
InstalledIntegrations,
|
||||
@@ -340,13 +339,6 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: false,
|
||||
key: 'LOGIN',
|
||||
},
|
||||
{
|
||||
path: ROUTES.FORGOT_PASSWORD,
|
||||
exact: true,
|
||||
component: ForgotPassword,
|
||||
isPrivate: false,
|
||||
key: 'FORGOT_PASSWORD',
|
||||
},
|
||||
{
|
||||
path: ROUTES.UN_AUTHORIZED,
|
||||
exact: true,
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'yarn generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../index';
|
||||
import type {
|
||||
GetFieldsKeys200,
|
||||
GetFieldsKeysParams,
|
||||
GetFieldsValues200,
|
||||
GetFieldsValuesParams,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
type AwaitedInput<T> = PromiseLike<T> | T;
|
||||
|
||||
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
|
||||
|
||||
/**
|
||||
* This endpoint returns field keys
|
||||
* @summary Get field keys
|
||||
*/
|
||||
export const getFieldsKeys = (
|
||||
params?: GetFieldsKeysParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetFieldsKeys200>({
|
||||
url: `/api/v1/fields/keys`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetFieldsKeysQueryKey = (params?: GetFieldsKeysParams) => {
|
||||
return ['getFieldsKeys', ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getGetFieldsKeysQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getFieldsKeys>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(
|
||||
params?: GetFieldsKeysParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getFieldsKeys>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetFieldsKeysQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getFieldsKeys>>> = ({
|
||||
signal,
|
||||
}) => getFieldsKeys(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getFieldsKeys>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetFieldsKeysQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getFieldsKeys>>
|
||||
>;
|
||||
export type GetFieldsKeysQueryError = RenderErrorResponseDTO;
|
||||
|
||||
/**
|
||||
* @summary Get field keys
|
||||
*/
|
||||
|
||||
export function useGetFieldsKeys<
|
||||
TData = Awaited<ReturnType<typeof getFieldsKeys>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(
|
||||
params?: GetFieldsKeysParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getFieldsKeys>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetFieldsKeysQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get field keys
|
||||
*/
|
||||
export const invalidateGetFieldsKeys = async (
|
||||
queryClient: QueryClient,
|
||||
params?: GetFieldsKeysParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetFieldsKeysQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint returns field values
|
||||
* @summary Get field values
|
||||
*/
|
||||
export const getFieldsValues = (
|
||||
params?: GetFieldsValuesParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetFieldsValues200>({
|
||||
url: `/api/v1/fields/values`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetFieldsValuesQueryKey = (params?: GetFieldsValuesParams) => {
|
||||
return ['getFieldsValues', ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getGetFieldsValuesQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getFieldsValues>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(
|
||||
params?: GetFieldsValuesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getFieldsValues>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetFieldsValuesQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getFieldsValues>>> = ({
|
||||
signal,
|
||||
}) => getFieldsValues(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getFieldsValues>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetFieldsValuesQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getFieldsValues>>
|
||||
>;
|
||||
export type GetFieldsValuesQueryError = RenderErrorResponseDTO;
|
||||
|
||||
/**
|
||||
* @summary Get field values
|
||||
*/
|
||||
|
||||
export function useGetFieldsValues<
|
||||
TData = Awaited<ReturnType<typeof getFieldsValues>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(
|
||||
params?: GetFieldsValuesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getFieldsValues>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetFieldsValuesQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get field values
|
||||
*/
|
||||
export const invalidateGetFieldsValues = async (
|
||||
queryClient: QueryClient,
|
||||
params?: GetFieldsValuesParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetFieldsValuesQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
@@ -1049,7 +1049,7 @@ export interface Querybuildertypesv5OrderByKeyDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
name?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -1141,79 +1141,6 @@ export interface RoletypesRoleDTO {
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type TelemetrytypesGettableFieldKeysDTOKeys = {
|
||||
[key: string]: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
} | null;
|
||||
|
||||
export interface TelemetrytypesGettableFieldKeysDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
complete: boolean;
|
||||
/**
|
||||
* @type object
|
||||
* @nullable true
|
||||
*/
|
||||
keys: TelemetrytypesGettableFieldKeysDTOKeys;
|
||||
}
|
||||
|
||||
export interface TelemetrytypesGettableFieldValuesDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
complete: boolean;
|
||||
values: TelemetrytypesTelemetryFieldValuesDTO;
|
||||
}
|
||||
|
||||
export interface TelemetrytypesTelemetryFieldKeyDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
fieldContext?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
fieldDataType?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
signal?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export interface TelemetrytypesTelemetryFieldValuesDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
boolValues?: boolean[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
numberValues?: number[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
relatedValues?: string[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
stringValues?: string[];
|
||||
}
|
||||
|
||||
export interface TypesChangePasswordRequestDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -1661,132 +1588,6 @@ export type DeleteAuthDomainPathParameters = {
|
||||
export type UpdateAuthDomainPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetFieldsKeysParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
signal?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
source?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
* @description undefined
|
||||
*/
|
||||
startUnixMilli?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
* @description undefined
|
||||
*/
|
||||
endUnixMilli?: number;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
fieldContext?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
fieldDataType?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
metricName?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
searchText?: string;
|
||||
};
|
||||
|
||||
export type GetFieldsKeys200 = {
|
||||
data?: TelemetrytypesGettableFieldKeysDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export type GetFieldsValuesParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
signal?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
source?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
* @description undefined
|
||||
*/
|
||||
startUnixMilli?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
* @description undefined
|
||||
*/
|
||||
endUnixMilli?: number;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
fieldContext?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
fieldDataType?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
metricName?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
searchText?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
existingQuery?: string;
|
||||
};
|
||||
|
||||
export type GetFieldsValues200 = {
|
||||
data?: TelemetrytypesGettableFieldValuesDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export type GetResetPasswordTokenPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
1
frontend/src/auto-import-registry.d.ts
vendored
1
frontend/src/auto-import-registry.d.ts
vendored
@@ -18,7 +18,6 @@ import '@signozhq/checkbox';
|
||||
import '@signozhq/combobox';
|
||||
import '@signozhq/command';
|
||||
import '@signozhq/design-tokens';
|
||||
import '@signozhq/icons';
|
||||
import '@signozhq/input';
|
||||
import '@signozhq/popover';
|
||||
import '@signozhq/resizable';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const ROUTES = {
|
||||
SIGN_UP: '/signup',
|
||||
LOGIN: '/login',
|
||||
FORGOT_PASSWORD: '/forgot-password',
|
||||
HOME: '/home',
|
||||
SERVICE_METRICS: '/services/:servicename',
|
||||
SERVICE_TOP_LEVEL_OPERATIONS: '/services/:servicename/top-level-operations',
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
.forgot-password-title {
|
||||
font-family: var(--label-large-600-font-family);
|
||||
font-size: var(--label-large-600-font-size);
|
||||
font-weight: var(--label-large-600-font-weight);
|
||||
letter-spacing: var(--label-large-600-letter-spacing);
|
||||
line-height: 1.45;
|
||||
color: var(--l1-foreground);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.forgot-password-description {
|
||||
font-family: var(--paragraph-base-400-font-family);
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
font-weight: var(--paragraph-base-400-font-weight);
|
||||
line-height: var(--paragraph-base-400-line-height);
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--l2-foreground);
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
max-width: 317px;
|
||||
}
|
||||
|
||||
.forgot-password-form {
|
||||
width: 100%;
|
||||
|
||||
// Label styling
|
||||
.forgot-password-label {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--l1-foreground);
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
|
||||
.lightMode & {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
// Parent container for fields
|
||||
.forgot-password-field {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.ant-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-password-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
|
||||
> .forgot-password-back-button,
|
||||
> .login-submit-btn {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-password-back-button {
|
||||
height: 32px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 2px;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: var(--l3-background);
|
||||
border: 1px solid var(--l3-border);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--l3-border);
|
||||
border-color: var(--l3-border);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Button } from '@signozhq/button';
|
||||
import { ArrowLeft, Mail } from '@signozhq/icons';
|
||||
|
||||
interface SuccessScreenProps {
|
||||
onBackToLogin: () => void;
|
||||
}
|
||||
|
||||
function SuccessScreen({ onBackToLogin }: SuccessScreenProps): JSX.Element {
|
||||
return (
|
||||
<div className="login-form-container">
|
||||
<div className="forgot-password-form">
|
||||
<div className="login-form-header">
|
||||
<div className="login-form-emoji">
|
||||
<Mail size={32} />
|
||||
</div>
|
||||
<h4 className="forgot-password-title">Check your email</h4>
|
||||
<p className="forgot-password-description">
|
||||
We've sent a password reset link to your email. Please check your
|
||||
inbox and follow the instructions to reset your password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="login-form-actions forgot-password-actions">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
type="button"
|
||||
data-testid="back-to-login"
|
||||
className="login-submit-btn"
|
||||
onClick={onBackToLogin}
|
||||
prefixIcon={<ArrowLeft size={12} />}
|
||||
>
|
||||
Back to login
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SuccessScreen;
|
||||
@@ -1,402 +0,0 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import {
|
||||
createErrorResponse,
|
||||
handleInternalServerError,
|
||||
rest,
|
||||
server,
|
||||
} from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { OrgSessionContext } from 'types/api/v2/sessions/context/get';
|
||||
|
||||
import ForgotPassword, { ForgotPasswordRouteState } from '../index';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
push: jest.fn(),
|
||||
location: {
|
||||
search: '',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const mockHistoryPush = history.push as jest.MockedFunction<
|
||||
typeof history.push
|
||||
>;
|
||||
|
||||
const FORGOT_PASSWORD_ENDPOINT = '*/api/v2/factor_password/forgot';
|
||||
|
||||
// Mock data
|
||||
const mockSingleOrg: OrgSessionContext[] = [
|
||||
{
|
||||
id: 'org-1',
|
||||
name: 'Test Organization',
|
||||
authNSupport: {
|
||||
password: [{ provider: 'email_password' }],
|
||||
callback: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockMultipleOrgs: OrgSessionContext[] = [
|
||||
{
|
||||
id: 'org-1',
|
||||
name: 'Organization One',
|
||||
authNSupport: {
|
||||
password: [{ provider: 'email_password' }],
|
||||
callback: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'org-2',
|
||||
name: 'Organization Two',
|
||||
authNSupport: {
|
||||
password: [{ provider: 'email_password' }],
|
||||
callback: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const TEST_EMAIL = 'jest.test@signoz.io';
|
||||
|
||||
const defaultProps: ForgotPasswordRouteState = {
|
||||
email: TEST_EMAIL,
|
||||
orgs: mockSingleOrg,
|
||||
};
|
||||
|
||||
const multiOrgProps: ForgotPasswordRouteState = {
|
||||
email: TEST_EMAIL,
|
||||
orgs: mockMultipleOrgs,
|
||||
};
|
||||
|
||||
describe('ForgotPassword Component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
describe('Initial Render', () => {
|
||||
it('renders forgot password form with all required elements', () => {
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/forgot your password\?/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/send a reset link to your inbox/i),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('email')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('forgot-password-submit')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('forgot-password-back')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pre-fills email from props', () => {
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const emailInput = screen.getByTestId('email');
|
||||
expect(emailInput).toHaveValue(TEST_EMAIL);
|
||||
});
|
||||
|
||||
it('disables email input field', () => {
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const emailInput = screen.getByTestId('email');
|
||||
expect(emailInput).toBeDisabled();
|
||||
});
|
||||
|
||||
it('does not show organization dropdown for single org', () => {
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
expect(screen.queryByTestId('orgId')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Organization Name')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('enables submit button when email is provided with single org', () => {
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple Organizations', () => {
|
||||
it('shows organization dropdown when multiple orgs exist', () => {
|
||||
render(<ForgotPassword {...multiOrgProps} />);
|
||||
|
||||
expect(screen.getByTestId('orgId')).toBeInTheDocument();
|
||||
expect(screen.getByText('Organization Name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables submit button when org is not selected', () => {
|
||||
const propsWithoutOrgId: ForgotPasswordRouteState = {
|
||||
email: TEST_EMAIL,
|
||||
orgs: mockMultipleOrgs,
|
||||
};
|
||||
|
||||
render(<ForgotPassword {...propsWithoutOrgId} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('enables submit button after selecting an organization', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<ForgotPassword {...multiOrgProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// Click on the dropdown to reveal the options
|
||||
await user.click(screen.getByRole('combobox'));
|
||||
await user.click(screen.getByText('Organization One'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('pre-selects organization when orgId is provided', () => {
|
||||
const propsWithOrgId: ForgotPasswordRouteState = {
|
||||
email: TEST_EMAIL,
|
||||
orgId: 'org-1',
|
||||
orgs: mockMultipleOrgs,
|
||||
};
|
||||
|
||||
render(<ForgotPassword {...propsWithOrgId} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Submission - Success', () => {
|
||||
it('successfully submits forgot password request and shows success screen', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(FORGOT_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success' })),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(await screen.findByText(/check your email/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/we've sent a password reset link/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows back to login button on success screen', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(FORGOT_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success' })),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(await screen.findByTestId('back-to-login')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redirects to login when clicking back to login on success screen', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(FORGOT_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success' })),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(await screen.findByTestId('back-to-login')).toBeInTheDocument();
|
||||
|
||||
const backToLoginButton = screen.getByTestId('back-to-login');
|
||||
await user.click(backToLoginButton);
|
||||
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith(ROUTES.LOGIN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Submission - Error Handling', () => {
|
||||
it('displays error message when forgot password API fails', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(
|
||||
FORGOT_PASSWORD_ENDPOINT,
|
||||
createErrorResponse(400, 'USER_NOT_FOUND', 'User not found'),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(await screen.findByText(/user not found/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays error message when API returns server error', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(rest.post(FORGOT_PASSWORD_ENDPOINT, handleInternalServerError));
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(
|
||||
await screen.findByText(/internal server error occurred/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clears error message on new submission attempt', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
let requestCount = 0;
|
||||
|
||||
server.use(
|
||||
rest.post(FORGOT_PASSWORD_ENDPOINT, (_req, res, ctx) => {
|
||||
requestCount += 1;
|
||||
if (requestCount === 1) {
|
||||
return res(
|
||||
ctx.status(400),
|
||||
ctx.json({
|
||||
error: {
|
||||
code: 'USER_NOT_FOUND',
|
||||
message: 'User not found',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return res(ctx.status(200), ctx.json({ status: 'success' }));
|
||||
}),
|
||||
);
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(await screen.findByText(/user not found/i)).toBeInTheDocument();
|
||||
|
||||
// Click submit again
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/user not found/i)).not.toBeInTheDocument();
|
||||
});
|
||||
expect(await screen.findByText(/check your email/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('redirects to login when clicking back button on form', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const backButton = screen.getByTestId('forgot-password-back');
|
||||
await user.click(backButton);
|
||||
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith(ROUTES.LOGIN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loading States', () => {
|
||||
it('shows loading state during API call', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(FORGOT_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(ctx.delay(100), ctx.status(200), ctx.json({ status: 'success' })),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
// Button should show loading state
|
||||
expect(await screen.findByText(/sending\.\.\./i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables submit button during loading', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(FORGOT_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(ctx.delay(100), ctx.status(200), ctx.json({ status: 'success' })),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('handles empty email gracefully', () => {
|
||||
const propsWithEmptyEmail: ForgotPasswordRouteState = {
|
||||
email: '',
|
||||
orgs: mockSingleOrg,
|
||||
};
|
||||
|
||||
render(<ForgotPassword {...propsWithEmptyEmail} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('handles whitespace-only email', () => {
|
||||
const propsWithWhitespaceEmail: ForgotPasswordRouteState = {
|
||||
email: ' ',
|
||||
orgs: mockSingleOrg,
|
||||
};
|
||||
|
||||
render(<ForgotPassword {...propsWithWhitespaceEmail} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('handles empty orgs array by disabling submission', () => {
|
||||
const propsWithNoOrgs: ForgotPasswordRouteState = {
|
||||
email: TEST_EMAIL,
|
||||
orgs: [],
|
||||
};
|
||||
|
||||
render(<ForgotPassword {...propsWithNoOrgs} />);
|
||||
|
||||
// Should not show org dropdown
|
||||
expect(screen.queryByTestId('orgId')).not.toBeInTheDocument();
|
||||
// Submit should be disabled because no orgId can be determined
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,217 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { ArrowLeft, ArrowRight } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Form, Select } from 'antd';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { useForgotPassword } from 'api/generated/services/users';
|
||||
import { AxiosError } from 'axios';
|
||||
import AuthError from 'components/AuthError/AuthError';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { OrgSessionContext } from 'types/api/v2/sessions/context/get';
|
||||
|
||||
import SuccessScreen from './SuccessScreen';
|
||||
|
||||
import './ForgotPassword.styles.scss';
|
||||
import 'container/Login/Login.styles.scss';
|
||||
|
||||
type FormValues = {
|
||||
email: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type ForgotPasswordRouteState = {
|
||||
email: string;
|
||||
orgId?: string;
|
||||
orgs: OrgSessionContext[];
|
||||
};
|
||||
|
||||
function ForgotPassword({
|
||||
email,
|
||||
orgId,
|
||||
orgs,
|
||||
}: ForgotPasswordRouteState): JSX.Element {
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const {
|
||||
mutate: forgotPasswordMutate,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
error: mutationError,
|
||||
} = useForgotPassword();
|
||||
|
||||
const errorMessage = useMemo(() => {
|
||||
if (!mutationError) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
ErrorResponseHandlerV2(mutationError as AxiosError<ErrorV2Resp>);
|
||||
} catch (apiError) {
|
||||
return apiError as APIError;
|
||||
}
|
||||
}, [mutationError]);
|
||||
|
||||
const initialOrgId = useMemo((): string | undefined => {
|
||||
if (orgId) {
|
||||
return orgId;
|
||||
}
|
||||
|
||||
if (orgs.length === 1) {
|
||||
return orgs[0]?.id;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [orgId, orgs]);
|
||||
|
||||
const watchedEmail = Form.useWatch('email', form);
|
||||
const selectedOrgId = Form.useWatch('orgId', form);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
email,
|
||||
orgId: initialOrgId,
|
||||
});
|
||||
}, [email, form, initialOrgId]);
|
||||
|
||||
const hasMultipleOrgs = orgs.length > 1;
|
||||
|
||||
const isSubmitEnabled = useMemo((): boolean => {
|
||||
if (isLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!watchedEmail?.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure we have an orgId (either selected from dropdown or the initial one)
|
||||
const currentOrgId = hasMultipleOrgs ? selectedOrgId : initialOrgId;
|
||||
return Boolean(currentOrgId);
|
||||
}, [watchedEmail, selectedOrgId, isLoading, initialOrgId, hasMultipleOrgs]);
|
||||
|
||||
const handleSubmit = useCallback((): void => {
|
||||
const values = form.getFieldsValue();
|
||||
const currentOrgId = hasMultipleOrgs ? values.orgId : initialOrgId;
|
||||
|
||||
if (!currentOrgId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the forgot password API
|
||||
forgotPasswordMutate({
|
||||
data: {
|
||||
email: values.email,
|
||||
orgId: currentOrgId,
|
||||
frontendBaseURL: window.location.origin,
|
||||
},
|
||||
});
|
||||
}, [form, forgotPasswordMutate, initialOrgId, hasMultipleOrgs]);
|
||||
|
||||
const handleBackToLogin = useCallback((): void => {
|
||||
history.push(ROUTES.LOGIN);
|
||||
}, []);
|
||||
|
||||
// Success screen
|
||||
if (isSuccess) {
|
||||
return <SuccessScreen onBackToLogin={handleBackToLogin} />;
|
||||
}
|
||||
|
||||
// Form screen
|
||||
return (
|
||||
<div className="login-form-container">
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleSubmit}
|
||||
className="forgot-password-form"
|
||||
initialValues={{
|
||||
email,
|
||||
orgId: initialOrgId,
|
||||
}}
|
||||
>
|
||||
<div className="login-form-header">
|
||||
<div className="login-form-emoji">
|
||||
<img src="/svgs/tv.svg" alt="TV" width="32" height="32" />
|
||||
</div>
|
||||
<h4 className="forgot-password-title">Forgot your password?</h4>
|
||||
<p className="forgot-password-description">
|
||||
Send a reset link to your inbox and get back to monitoring.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="login-form-card">
|
||||
<div className="forgot-password-field">
|
||||
<label className="forgot-password-label" htmlFor="forgotPasswordEmail">
|
||||
Email address
|
||||
</label>
|
||||
<Form.Item name="email">
|
||||
<Input
|
||||
type="email"
|
||||
id="forgotPasswordEmail"
|
||||
data-testid="email"
|
||||
required
|
||||
disabled
|
||||
className="login-form-input"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{hasMultipleOrgs && (
|
||||
<div className="forgot-password-field">
|
||||
<label className="forgot-password-label" htmlFor="orgId">
|
||||
Organization Name
|
||||
</label>
|
||||
<Form.Item
|
||||
name="orgId"
|
||||
rules={[{ required: true, message: 'Please select your organization' }]}
|
||||
>
|
||||
<Select
|
||||
id="orgId"
|
||||
data-testid="orgId"
|
||||
className="login-form-input login-form-select-no-border"
|
||||
placeholder="Select your organization"
|
||||
options={orgs.map((org) => ({
|
||||
value: org.id,
|
||||
label: org.name || 'default',
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{errorMessage && <AuthError error={errorMessage} />}
|
||||
|
||||
<div className="login-form-actions forgot-password-actions">
|
||||
<Button
|
||||
variant="solid"
|
||||
type="button"
|
||||
data-testid="forgot-password-back"
|
||||
className="forgot-password-back-button"
|
||||
onClick={handleBackToLogin}
|
||||
prefixIcon={<ArrowLeft size={12} />}
|
||||
>
|
||||
Back to login
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
disabled={!isSubmitEnabled}
|
||||
loading={isLoading}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
type="submit"
|
||||
data-testid="forgot-password-submit"
|
||||
className="login-submit-btn"
|
||||
suffixIcon={<ArrowRight size={12} />}
|
||||
>
|
||||
{isLoading ? 'Sending...' : 'Send reset link'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ForgotPassword;
|
||||
@@ -407,10 +407,6 @@
|
||||
color: var(--text-neutral-light-200) !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--text-ink-500) !important;
|
||||
}
|
||||
|
||||
&:hover .ant-select-selector {
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Form, Input, Select, Typography } from 'antd';
|
||||
import { Form, Input, Select, Tooltip, Typography } from 'antd';
|
||||
import getVersion from 'api/v1/version/get';
|
||||
import get from 'api/v2/sessions/context/get';
|
||||
import post from 'api/v2/sessions/email_password/post';
|
||||
@@ -220,20 +220,6 @@ function Login(): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const handleForgotPasswordClick = useCallback((): void => {
|
||||
const email = form.getFieldValue('email');
|
||||
|
||||
if (!email || !sessionsContext || !sessionsContext?.orgs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
history.push(ROUTES.FORGOT_PASSWORD, {
|
||||
email,
|
||||
orgId: sessionsOrgId,
|
||||
orgs: sessionsContext.orgs,
|
||||
});
|
||||
}, [form, sessionsContext, sessionsOrgId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (callbackAuthError) {
|
||||
setErrorMessage(
|
||||
@@ -359,16 +345,11 @@ function Login(): JSX.Element {
|
||||
<ParentContainer>
|
||||
<div className="password-label-container">
|
||||
<Label htmlFor="Password">Password</Label>
|
||||
<Typography.Link
|
||||
className="forgot-password-link"
|
||||
href="#"
|
||||
onClick={(event): void => {
|
||||
event.preventDefault();
|
||||
handleForgotPasswordClick();
|
||||
}}
|
||||
>
|
||||
Forgot password?
|
||||
</Typography.Link>
|
||||
<Tooltip title="Ask your admin to reset your password and send you a new invite link">
|
||||
<Typography.Link className="forgot-password-link">
|
||||
Forgot password?
|
||||
</Typography.Link>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<FormContainer.Item name="password">
|
||||
<Input.Password
|
||||
|
||||
@@ -103,7 +103,7 @@ export const getTotalLogSizeWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.SUM,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
@@ -140,7 +140,7 @@ export const getTotalTraceSizeWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.SUM,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
@@ -177,7 +177,7 @@ export const getTotalMetricDatapointCountWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.SUM,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
@@ -214,7 +214,7 @@ export const getLogCountWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
@@ -251,7 +251,7 @@ export const getLogSizeWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
@@ -288,7 +288,7 @@ export const getSpanCountWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
@@ -325,7 +325,7 @@ export const getSpanSizeWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
@@ -362,7 +362,7 @@ export const getMetricCountWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -323,6 +323,7 @@
|
||||
"java instrumentation",
|
||||
"java k8s",
|
||||
"java logs",
|
||||
"java metrics",
|
||||
"java microservices",
|
||||
"java monitoring",
|
||||
"java observability",
|
||||
@@ -331,12 +332,10 @@
|
||||
"java vm",
|
||||
"java windows",
|
||||
"jboss",
|
||||
"wildfly",
|
||||
"jdk 11",
|
||||
"jdk 17",
|
||||
"jdk 21",
|
||||
"jdk 8",
|
||||
"jdbc",
|
||||
"opentelemetry java",
|
||||
"quarkus",
|
||||
"spring boot",
|
||||
@@ -373,12 +372,6 @@
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-springboot/"
|
||||
},
|
||||
{
|
||||
"key": "docker",
|
||||
"label": "Docker",
|
||||
"imgUrl": "/Logos/docker.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-springboot/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
@@ -410,12 +403,6 @@
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-tomcat/"
|
||||
},
|
||||
{
|
||||
"key": "docker",
|
||||
"label": "Docker",
|
||||
"imgUrl": "/Logos/docker.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-tomcat/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
@@ -427,7 +414,7 @@
|
||||
},
|
||||
{
|
||||
"key": "jboss",
|
||||
"label": "JBoss/WildFly",
|
||||
"label": "JBoss",
|
||||
"imgUrl": "/Logos/jboss.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-jboss/",
|
||||
"question": {
|
||||
@@ -447,12 +434,6 @@
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-jboss/"
|
||||
},
|
||||
{
|
||||
"key": "docker",
|
||||
"label": "Docker",
|
||||
"imgUrl": "/Logos/docker.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-jboss/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
@@ -484,12 +465,6 @@
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-quarkus/"
|
||||
},
|
||||
{
|
||||
"key": "docker",
|
||||
"label": "Docker",
|
||||
"imgUrl": "/Logos/docker.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-quarkus/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
@@ -520,18 +495,6 @@
|
||||
"label": "Kubernetes",
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-java/"
|
||||
},
|
||||
{
|
||||
"key": "docker",
|
||||
"label": "Docker",
|
||||
"imgUrl": "/Logos/docker.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-java/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
"imgUrl": "/Logos/windows.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-java/"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -567,6 +530,7 @@
|
||||
"python in containers",
|
||||
"python instrumentation",
|
||||
"python k8s",
|
||||
"python metrics",
|
||||
"python microservices",
|
||||
"python observability",
|
||||
"python on kubernetes",
|
||||
@@ -619,6 +583,7 @@
|
||||
],
|
||||
"module": "apm",
|
||||
"relatedSearchKeywords": [
|
||||
"angular",
|
||||
"apm",
|
||||
"apm/traces",
|
||||
"application performance monitoring",
|
||||
@@ -630,6 +595,7 @@
|
||||
"next.js",
|
||||
"nodejs",
|
||||
"nuxtjs",
|
||||
"reactjs",
|
||||
"traces",
|
||||
"tracing",
|
||||
"ts",
|
||||
@@ -644,8 +610,8 @@
|
||||
"entityID": "framework",
|
||||
"options": [
|
||||
{
|
||||
"key": "nodejs-nestjs",
|
||||
"label": "NodeJs/NestJS",
|
||||
"key": "nodejs",
|
||||
"label": "NodeJs",
|
||||
"imgUrl": "/Logos/nodejs.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-javascript/",
|
||||
"question": {
|
||||
@@ -666,16 +632,103 @@
|
||||
"link": "/docs/instrumentation/opentelemetry-javascript/"
|
||||
},
|
||||
{
|
||||
"key": "docker",
|
||||
"label": "Docker",
|
||||
"imgUrl": "/Logos/docker.svg",
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
"imgUrl": "/Logos/windows.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-javascript/"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "express",
|
||||
"label": "Express",
|
||||
"imgUrl": "/Logos/express.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-express/",
|
||||
"question": {
|
||||
"desc": "What is your Environment?",
|
||||
"type": "select",
|
||||
"entityID": "environment",
|
||||
"options": [
|
||||
{
|
||||
"key": "vm",
|
||||
"label": "VM",
|
||||
"imgUrl": "/Logos/vm.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-express/"
|
||||
},
|
||||
{
|
||||
"key": "k8s",
|
||||
"label": "Kubernetes",
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-express/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
"imgUrl": "/Logos/windows.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-javascript/"
|
||||
"link": "/docs/instrumentation/opentelemetry-express/"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "nestjs",
|
||||
"label": "NestJS",
|
||||
"imgUrl": "/Logos/nestjs.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-nestjs/",
|
||||
"question": {
|
||||
"desc": "What is your Environment?",
|
||||
"type": "select",
|
||||
"entityID": "environment",
|
||||
"options": [
|
||||
{
|
||||
"key": "vm",
|
||||
"label": "VM",
|
||||
"imgUrl": "/Logos/vm.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-nestjs/"
|
||||
},
|
||||
{
|
||||
"key": "k8s",
|
||||
"label": "Kubernetes",
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-nestjs/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
"imgUrl": "/Logos/windows.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-nestjs/"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "angular",
|
||||
"label": "Angular",
|
||||
"imgUrl": "/Logos/angular.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-angular/",
|
||||
"question": {
|
||||
"desc": "What is your Environment?",
|
||||
"type": "select",
|
||||
"entityID": "environment",
|
||||
"options": [
|
||||
{
|
||||
"key": "vm",
|
||||
"label": "VM",
|
||||
"imgUrl": "/Logos/vm.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-angular/"
|
||||
},
|
||||
{
|
||||
"key": "k8s",
|
||||
"label": "Kubernetes",
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-angular/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
"imgUrl": "/Logos/windows.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-angular/"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -703,16 +756,41 @@
|
||||
"link": "/docs/instrumentation/opentelemetry-nextjs/"
|
||||
},
|
||||
{
|
||||
"key": "docker",
|
||||
"label": "Docker",
|
||||
"imgUrl": "/Logos/docker.svg",
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
"imgUrl": "/Logos/windows.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-nextjs/"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "reactjs",
|
||||
"label": "ReactJS",
|
||||
"imgUrl": "/Logos/reactjs.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-reactjs/",
|
||||
"question": {
|
||||
"desc": "What is your Environment?",
|
||||
"type": "select",
|
||||
"entityID": "environment",
|
||||
"options": [
|
||||
{
|
||||
"key": "vm",
|
||||
"label": "VM",
|
||||
"imgUrl": "/Logos/vm.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-reactjs/"
|
||||
},
|
||||
{
|
||||
"key": "k8s",
|
||||
"label": "Kubernetes",
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-reactjs/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
"imgUrl": "/Logos/windows.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-nextjs/"
|
||||
"link": "/docs/instrumentation/opentelemetry-reactjs/"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -739,12 +817,6 @@
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-nuxtjs/"
|
||||
},
|
||||
{
|
||||
"key": "docker",
|
||||
"label": "Docker",
|
||||
"imgUrl": "/Logos/docker.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-nuxtjs/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
@@ -758,7 +830,32 @@
|
||||
"key": "react-native",
|
||||
"label": "React Native",
|
||||
"imgUrl": "/Logos/reactjs.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-react-native/"
|
||||
"link": "/docs/instrumentation/opentelemetry-react-native/",
|
||||
"question": {
|
||||
"desc": "What is your Environment?",
|
||||
"type": "select",
|
||||
"entityID": "environment",
|
||||
"options": [
|
||||
{
|
||||
"key": "vm",
|
||||
"label": "VM",
|
||||
"imgUrl": "/Logos/vm.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-react-native/"
|
||||
},
|
||||
{
|
||||
"key": "k8s",
|
||||
"label": "Kubernetes",
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-react-native/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
"imgUrl": "/Logos/windows.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-react-native/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -792,6 +889,7 @@
|
||||
"golang",
|
||||
"golang apm",
|
||||
"golang instrumentation",
|
||||
"golang metrics",
|
||||
"golang observability",
|
||||
"golang performance monitoring",
|
||||
"golang tracing",
|
||||
@@ -882,39 +980,69 @@
|
||||
"php-fpm monitoring",
|
||||
"slim php",
|
||||
"traces",
|
||||
"tracing",
|
||||
"wordpress"
|
||||
"tracing"
|
||||
],
|
||||
"id": "php",
|
||||
"link": "/docs/instrumentation/opentelemetry-php/",
|
||||
"question": {
|
||||
"desc": "What is your Environment?",
|
||||
"desc": "Which PHP framework do you use?",
|
||||
"type": "select",
|
||||
"entityID": "environment",
|
||||
"entityID": "framework",
|
||||
"options": [
|
||||
{
|
||||
"key": "vm",
|
||||
"label": "VM",
|
||||
"imgUrl": "/Logos/vm.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-php/"
|
||||
"key": "laravel",
|
||||
"label": "Laravel",
|
||||
"imgUrl": "/Logos/laravel.svg",
|
||||
"link": "/docs/instrumentation/laravel/",
|
||||
"question": {
|
||||
"desc": "What is your Environment?",
|
||||
"type": "select",
|
||||
"entityID": "environment",
|
||||
"options": [
|
||||
{
|
||||
"key": "vm",
|
||||
"label": "VM",
|
||||
"imgUrl": "/Logos/vm.svg",
|
||||
"link": "/docs/instrumentation/laravel/"
|
||||
},
|
||||
{
|
||||
"key": "k8s",
|
||||
"label": "Kubernetes",
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/laravel/"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "k8s",
|
||||
"label": "Kubernetes",
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-php/"
|
||||
},
|
||||
{
|
||||
"key": "docker",
|
||||
"label": "Docker",
|
||||
"imgUrl": "/Logos/docker.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-php/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
"imgUrl": "/Logos/windows.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-php/"
|
||||
"key": "Others",
|
||||
"label": "Others",
|
||||
"imgUrl": "/Logos/php-others.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-php/",
|
||||
"question": {
|
||||
"desc": "What is your Environment?",
|
||||
"type": "select",
|
||||
"entityID": "environment",
|
||||
"options": [
|
||||
{
|
||||
"key": "vm",
|
||||
"label": "VM",
|
||||
"imgUrl": "/Logos/vm.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-php/"
|
||||
},
|
||||
{
|
||||
"key": "k8s",
|
||||
"label": "Kubernetes",
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-php/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
"imgUrl": "/Logos/windows.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-php/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -989,12 +1117,6 @@
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-dotnet/"
|
||||
},
|
||||
{
|
||||
"key": "docker",
|
||||
"label": "Docker",
|
||||
"imgUrl": "/Logos/docker.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-dotnet/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
@@ -1006,7 +1128,7 @@
|
||||
},
|
||||
{
|
||||
"dataSource": "ruby-on-rails",
|
||||
"label": "Ruby",
|
||||
"label": "Ruby on Rails",
|
||||
"imgUrl": "/Logos/ruby-on-rails.svg",
|
||||
"tags": [
|
||||
"apm/traces"
|
||||
@@ -1046,12 +1168,9 @@
|
||||
"ruby tracing",
|
||||
"ruby-on-rails",
|
||||
"traces",
|
||||
"tracing",
|
||||
"sinatra",
|
||||
"sidekiq",
|
||||
"resque"
|
||||
"tracing"
|
||||
],
|
||||
"id": "ruby",
|
||||
"id": "ruby-on-rails",
|
||||
"link": "/docs/instrumentation/opentelemetry-ruby-on-rails/",
|
||||
"question": {
|
||||
"desc": "What is your Environment?",
|
||||
@@ -1070,12 +1189,6 @@
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-ruby-on-rails/"
|
||||
},
|
||||
{
|
||||
"key": "docker",
|
||||
"label": "Docker",
|
||||
"imgUrl": "/Logos/docker.svg",
|
||||
"link": "/docs/instrumentation/opentelemetry-ruby-on-rails/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
@@ -1355,6 +1468,41 @@
|
||||
"id": "nginx-tracing",
|
||||
"link": "/docs/instrumentation/opentelemetry-nginx/"
|
||||
},
|
||||
{
|
||||
"dataSource": "opentelemetry-wordpress",
|
||||
"label": "WordPress",
|
||||
"imgUrl": "/Logos/wordpress.svg",
|
||||
"tags": [
|
||||
"apm/traces"
|
||||
],
|
||||
"module": "apm",
|
||||
"relatedSearchKeywords": [
|
||||
"apm",
|
||||
"apm/traces",
|
||||
"application performance monitoring",
|
||||
"monitor wordpress site",
|
||||
"opentelemetry",
|
||||
"opentelemetry wordpress",
|
||||
"opentelemetry-wordpress",
|
||||
"otel",
|
||||
"otel wordpress",
|
||||
"traces",
|
||||
"tracing",
|
||||
"wordpress",
|
||||
"wordpress apm",
|
||||
"wordpress instrumentation",
|
||||
"wordpress metrics",
|
||||
"wordpress monitoring",
|
||||
"wordpress observability",
|
||||
"wordpress performance",
|
||||
"wordpress php monitoring",
|
||||
"wordpress plugin monitoring",
|
||||
"wordpress to signoz",
|
||||
"wordpress tracing"
|
||||
],
|
||||
"id": "opentelemetry-wordpress",
|
||||
"link": "/docs/instrumentation/opentelemetry-wordpress/"
|
||||
},
|
||||
{
|
||||
"dataSource": "opentelemetry-cloudflare",
|
||||
"label": "Cloudflare Tracing",
|
||||
@@ -1419,25 +1567,6 @@
|
||||
"id": "opentelemetry-cloudflare-logs",
|
||||
"link": "/docs/logs-management/send-logs/cloudflare-logs/"
|
||||
},
|
||||
{
|
||||
"dataSource": "convex-logs",
|
||||
"label": "Convex Logs",
|
||||
"imgUrl": "/Logos/convex-logo.svg",
|
||||
"tags": [
|
||||
"logs"
|
||||
],
|
||||
"module": "logs",
|
||||
"relatedSearchKeywords": [
|
||||
"convex",
|
||||
"convex log streaming",
|
||||
"convex logs",
|
||||
"convex webhook",
|
||||
"logging",
|
||||
"logs"
|
||||
],
|
||||
"id": "convex-logs",
|
||||
"link": "/docs/logs-management/send-logs/convex-log-streams-signoz/"
|
||||
},
|
||||
{
|
||||
"dataSource": "kubernetes-pod-logs",
|
||||
"label": "Kubernetes Pod Logs",
|
||||
@@ -4439,7 +4568,6 @@
|
||||
],
|
||||
"module": "metrics",
|
||||
"relatedSearchKeywords": [
|
||||
"browser metrics",
|
||||
"core web vitals metrics",
|
||||
"frontend metrics",
|
||||
"frontend monitoring",
|
||||
@@ -4459,7 +4587,7 @@
|
||||
"svelte",
|
||||
"sveltekit",
|
||||
"nextjs",
|
||||
"next.js"
|
||||
"next.js"
|
||||
],
|
||||
"link": "/docs/frontend-monitoring/web-vitals-with-metrics/"
|
||||
},
|
||||
@@ -4494,7 +4622,7 @@
|
||||
"svelte",
|
||||
"sveltekit",
|
||||
"nextjs",
|
||||
"next.js"
|
||||
"next.js"
|
||||
],
|
||||
"link": "/docs/frontend-monitoring/web-vitals-with-traces/"
|
||||
},
|
||||
@@ -4536,7 +4664,7 @@
|
||||
"svelte",
|
||||
"sveltekit",
|
||||
"nextjs",
|
||||
"next.js"
|
||||
"next.js"
|
||||
],
|
||||
"link": "/docs/frontend-monitoring/document-load/"
|
||||
},
|
||||
@@ -5188,6 +5316,23 @@
|
||||
],
|
||||
"link": "/docs/metrics-management/mysql-metrics/"
|
||||
},
|
||||
{
|
||||
"dataSource": "jvm",
|
||||
"label": "JVM",
|
||||
"imgUrl": "/Logos/java.svg",
|
||||
"tags": [
|
||||
"metrics"
|
||||
],
|
||||
"module": "metrics",
|
||||
"relatedSearchKeywords": [
|
||||
"java virtual machine",
|
||||
"jvm",
|
||||
"jvm metrics",
|
||||
"jvm monitoring",
|
||||
"runtime"
|
||||
],
|
||||
"link": "/docs/tutorial/jvm-metrics/"
|
||||
},
|
||||
{
|
||||
"dataSource": "jmx",
|
||||
"label": "JMX",
|
||||
@@ -5203,7 +5348,7 @@
|
||||
"jmx monitoring",
|
||||
"runtime"
|
||||
],
|
||||
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-java/jmx-metrics/"
|
||||
"link": "/docs/tutorial/jmx-metrics/"
|
||||
},
|
||||
{
|
||||
"dataSource": "prometheus-metrics",
|
||||
@@ -5429,17 +5574,16 @@
|
||||
"svelte",
|
||||
"sveltekit",
|
||||
"nextjs",
|
||||
"next.js"
|
||||
"next.js"
|
||||
],
|
||||
"link": "/docs/frontend-monitoring/sending-logs-with-opentelemetry/"
|
||||
},
|
||||
{
|
||||
"dataSource": "frontend-traces",
|
||||
"label": "Frontend Tracing",
|
||||
"label": "Frontend Traces",
|
||||
"imgUrl": "/Logos/traces.svg",
|
||||
"tags": [
|
||||
"Frontend Monitoring",
|
||||
"apm/traces"
|
||||
"Frontend Monitoring"
|
||||
],
|
||||
"module": "apm",
|
||||
"relatedSearchKeywords": [
|
||||
@@ -5461,7 +5605,7 @@
|
||||
"svelte",
|
||||
"sveltekit",
|
||||
"nextjs",
|
||||
"next.js"
|
||||
"next.js"
|
||||
],
|
||||
"link": "/docs/frontend-monitoring/sending-traces-with-opentelemetry/"
|
||||
},
|
||||
@@ -5492,7 +5636,7 @@
|
||||
"svelte",
|
||||
"sveltekit",
|
||||
"nextjs",
|
||||
"next.js"
|
||||
"next.js"
|
||||
],
|
||||
"link": "/docs/frontend-monitoring/sending-metrics-with-opentelemetry/"
|
||||
},
|
||||
@@ -5539,6 +5683,7 @@
|
||||
"label": "Golang Metrics",
|
||||
"imgUrl": "/Logos/go.svg",
|
||||
"tags": [
|
||||
"apm/traces",
|
||||
"metrics"
|
||||
],
|
||||
"module": "metrics",
|
||||
@@ -5592,64 +5737,12 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"dataSource": "java-metrics",
|
||||
"label": "Java Metrics",
|
||||
"imgUrl": "/Logos/java.svg",
|
||||
"tags": [
|
||||
"metrics"
|
||||
],
|
||||
"module": "metrics",
|
||||
"relatedSearchKeywords": [
|
||||
"java",
|
||||
"java metrics",
|
||||
"java monitoring",
|
||||
"java observability",
|
||||
"jvm metrics",
|
||||
"metrics",
|
||||
"opentelemetry java",
|
||||
"otel java",
|
||||
"runtime metrics"
|
||||
],
|
||||
"id": "java-metrics",
|
||||
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-java/",
|
||||
"question": {
|
||||
"desc": "What is your Environment?",
|
||||
"type": "select",
|
||||
"entityID": "environment",
|
||||
"options": [
|
||||
{
|
||||
"key": "vm",
|
||||
"label": "VM",
|
||||
"imgUrl": "/Logos/vm.svg",
|
||||
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-java/"
|
||||
},
|
||||
{
|
||||
"key": "k8s",
|
||||
"label": "Kubernetes",
|
||||
"imgUrl": "/Logos/kubernetes.svg",
|
||||
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-java/"
|
||||
},
|
||||
{
|
||||
"key": "windows",
|
||||
"label": "Windows",
|
||||
"imgUrl": "/Logos/windows.svg",
|
||||
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-java/"
|
||||
},
|
||||
{
|
||||
"key": "docker",
|
||||
"label": "Docker",
|
||||
"imgUrl": "/Logos/docker.svg",
|
||||
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-java/"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"dataSource": "python-metrics",
|
||||
"label": "Python Metrics",
|
||||
"imgUrl": "/Logos/python.svg",
|
||||
"tags": [
|
||||
"apm/traces",
|
||||
"metrics"
|
||||
],
|
||||
"module": "metrics",
|
||||
@@ -5703,6 +5796,7 @@
|
||||
"label": ".NET Metrics",
|
||||
"imgUrl": "/Logos/dotnet.svg",
|
||||
"tags": [
|
||||
"apm/traces",
|
||||
"metrics"
|
||||
],
|
||||
"module": "metrics",
|
||||
@@ -5759,6 +5853,7 @@
|
||||
"label": "Node.js Metrics",
|
||||
"imgUrl": "/Logos/nodejs.svg",
|
||||
"tags": [
|
||||
"apm/traces",
|
||||
"metrics"
|
||||
],
|
||||
"module": "metrics",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { FilterOutlined, VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { Atom, Binoculars, SquareMousePointer, Terminal } from 'lucide-react';
|
||||
@@ -32,7 +32,6 @@ export default function LeftToolbarActions({
|
||||
<Tooltip title="Show Filters">
|
||||
<Button onClick={handleFilterVisibilityChange} className="filter-btn">
|
||||
<FilterOutlined />
|
||||
<VerticalAlignTopOutlined rotate={90} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: none;
|
||||
width: 40px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 12px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
|
||||
@@ -95,6 +95,7 @@ function ResourceAttributesFilter({
|
||||
data-testid="resource-environment-filter"
|
||||
style={{ minWidth: 200, height: 34 }}
|
||||
onChange={handleEnvironmentChange}
|
||||
onBlur={handleBlur}
|
||||
>
|
||||
{environments.map((opt) => (
|
||||
<Select.Option key={opt.value} value={opt.value}>
|
||||
|
||||
@@ -32,7 +32,6 @@ export const routeConfig: Record<string, QueryParams[]> = {
|
||||
[ROUTES.LIST_ALL_ALERT]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.LIST_LICENSES]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.LOGIN]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.FORGOT_PASSWORD]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.LOGS]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.LOGS_BASE]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.MY_SETTINGS]: [QueryParams.resourceAttributes],
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
import { whilelistedKeys } from '../config';
|
||||
import { mappingWithRoutesAndKeys } from '../utils';
|
||||
|
||||
describe('useResourceAttribute config', () => {
|
||||
describe('whilelistedKeys', () => {
|
||||
it('should include underscore-notation keys (DOT_METRICS_ENABLED=false)', () => {
|
||||
expect(whilelistedKeys).toContain('resource_deployment_environment');
|
||||
expect(whilelistedKeys).toContain('resource_k8s_cluster_name');
|
||||
expect(whilelistedKeys).toContain('resource_k8s_cluster_namespace');
|
||||
});
|
||||
|
||||
it('should include dot-notation keys (DOT_METRICS_ENABLED=true)', () => {
|
||||
expect(whilelistedKeys).toContain('resource_deployment.environment');
|
||||
expect(whilelistedKeys).toContain('resource_k8s.cluster.name');
|
||||
expect(whilelistedKeys).toContain('resource_k8s.cluster.namespace');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mappingWithRoutesAndKeys', () => {
|
||||
const dotNotationFilters = [
|
||||
{
|
||||
label: 'deployment.environment',
|
||||
value: 'resource_deployment.environment',
|
||||
},
|
||||
{ label: 'k8s.cluster.name', value: 'resource_k8s.cluster.name' },
|
||||
{ label: 'k8s.cluster.namespace', value: 'resource_k8s.cluster.namespace' },
|
||||
];
|
||||
|
||||
const underscoreNotationFilters = [
|
||||
{
|
||||
label: 'deployment.environment',
|
||||
value: 'resource_deployment_environment',
|
||||
},
|
||||
{ label: 'k8s.cluster.name', value: 'resource_k8s_cluster_name' },
|
||||
{ label: 'k8s.cluster.namespace', value: 'resource_k8s_cluster_namespace' },
|
||||
];
|
||||
|
||||
const nonWhitelistedFilters = [
|
||||
{ label: 'host.name', value: 'resource_host_name' },
|
||||
{ label: 'service.name', value: 'resource_service_name' },
|
||||
];
|
||||
|
||||
it('should keep dot-notation filters on the Service Map route', () => {
|
||||
const result = mappingWithRoutesAndKeys(
|
||||
ROUTES.SERVICE_MAP,
|
||||
dotNotationFilters,
|
||||
);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result).toEqual(dotNotationFilters);
|
||||
});
|
||||
|
||||
it('should keep underscore-notation filters on the Service Map route', () => {
|
||||
const result = mappingWithRoutesAndKeys(
|
||||
ROUTES.SERVICE_MAP,
|
||||
underscoreNotationFilters,
|
||||
);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result).toEqual(underscoreNotationFilters);
|
||||
});
|
||||
|
||||
it('should filter out non-whitelisted keys on the Service Map route', () => {
|
||||
const allFilters = [...dotNotationFilters, ...nonWhitelistedFilters];
|
||||
const result = mappingWithRoutesAndKeys(ROUTES.SERVICE_MAP, allFilters);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result).toEqual(dotNotationFilters);
|
||||
});
|
||||
|
||||
it('should return all filters on non-Service Map routes', () => {
|
||||
const allFilters = [...dotNotationFilters, ...nonWhitelistedFilters];
|
||||
const result = mappingWithRoutesAndKeys('/services', allFilters);
|
||||
expect(result).toHaveLength(5);
|
||||
expect(result).toEqual(allFilters);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,5 @@
|
||||
export const whilelistedKeys = [
|
||||
'resource_deployment_environment',
|
||||
'resource_deployment.environment',
|
||||
'resource_k8s_cluster_name',
|
||||
'resource_k8s.cluster.name',
|
||||
'resource_k8s_cluster_namespace',
|
||||
'resource_k8s.cluster.namespace',
|
||||
];
|
||||
|
||||
@@ -4,6 +4,7 @@ import uPlot, { Axis } from 'uplot';
|
||||
|
||||
import { uPlotXAxisValuesFormat } from '../../uPlotLib/utils/constants';
|
||||
import getGridColor from '../../uPlotLib/utils/getGridColor';
|
||||
import { buildYAxisSizeCalculator } from '../utils/axis';
|
||||
import { AxisProps, ConfigBuilder } from './types';
|
||||
|
||||
const PANEL_TYPES_WITH_X_AXIS_DATETIME_FORMAT = [
|
||||
@@ -114,81 +115,6 @@ export class UPlotAxisBuilder extends ConfigBuilder<AxisProps, Axis> {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate axis size from existing size property
|
||||
*/
|
||||
private getExistingAxisSize(
|
||||
self: uPlot,
|
||||
axis: Axis,
|
||||
values: string[] | undefined,
|
||||
axisIdx: number,
|
||||
cycleNum: number,
|
||||
): number {
|
||||
const internalSize = (axis as { _size?: number })._size;
|
||||
if (internalSize !== undefined) {
|
||||
return internalSize;
|
||||
}
|
||||
|
||||
const existingSize = axis.size;
|
||||
if (typeof existingSize === 'function') {
|
||||
return existingSize(self, values ?? [], axisIdx, cycleNum);
|
||||
}
|
||||
|
||||
return existingSize ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate text width for longest value
|
||||
*/
|
||||
private calculateTextWidth(
|
||||
self: uPlot,
|
||||
axis: Axis,
|
||||
values: string[] | undefined,
|
||||
): number {
|
||||
if (!values || values.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Find longest value
|
||||
const longestVal = values.reduce(
|
||||
(acc, val) => (val.length > acc.length ? val : acc),
|
||||
'',
|
||||
);
|
||||
|
||||
if (longestVal === '' || !axis.font?.[0]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-destructuring, no-param-reassign
|
||||
self.ctx.font = axis.font[0];
|
||||
return self.ctx.measureText(longestVal).width / devicePixelRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Y-axis dynamic size calculator
|
||||
*/
|
||||
private buildYAxisSizeCalculator(): uPlot.Axis.Size {
|
||||
return (
|
||||
self: uPlot,
|
||||
values: string[] | undefined,
|
||||
axisIdx: number,
|
||||
cycleNum: number,
|
||||
): number => {
|
||||
const axis = self.axes[axisIdx];
|
||||
|
||||
// Bail out, force convergence
|
||||
if (cycleNum > 1) {
|
||||
return this.getExistingAxisSize(self, axis, values, axisIdx, cycleNum);
|
||||
}
|
||||
|
||||
const gap = this.props.gap ?? 5;
|
||||
let axisSize = (axis.ticks?.size ?? 0) + gap;
|
||||
axisSize += this.calculateTextWidth(self, axis, values);
|
||||
|
||||
return Math.ceil(axisSize);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build dynamic size calculator for Y-axis
|
||||
*/
|
||||
@@ -202,7 +128,7 @@ export class UPlotAxisBuilder extends ConfigBuilder<AxisProps, Axis> {
|
||||
|
||||
// Y-axis needs dynamic sizing based on text width
|
||||
if (scaleKey === 'y') {
|
||||
return this.buildYAxisSizeCalculator();
|
||||
return buildYAxisSizeCalculator(this.props.gap ?? 5);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -15,7 +15,33 @@ import {
|
||||
* Builder for uPlot series configuration
|
||||
* Handles creation of series settings
|
||||
*/
|
||||
|
||||
/**
|
||||
* Path builders are static and shared across all instances of UPlotSeriesBuilder
|
||||
*/
|
||||
let builders: PathBuilders | null = null;
|
||||
export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
|
||||
constructor(props: SeriesProps) {
|
||||
super(props);
|
||||
const pathBuilders = uPlot.paths;
|
||||
|
||||
if (!builders) {
|
||||
const linearBuilder = pathBuilders.linear;
|
||||
const splineBuilder = pathBuilders.spline;
|
||||
const steppedBuilder = pathBuilders.stepped;
|
||||
|
||||
if (!linearBuilder || !splineBuilder || !steppedBuilder) {
|
||||
throw new Error('Required uPlot path builders are not available');
|
||||
}
|
||||
builders = {
|
||||
linear: linearBuilder(),
|
||||
spline: splineBuilder(),
|
||||
stepBefore: steppedBuilder({ align: -1 }),
|
||||
stepAfter: steppedBuilder({ align: 1 }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private buildLineConfig({
|
||||
lineColor,
|
||||
lineWidth,
|
||||
@@ -198,8 +224,6 @@ interface PathBuilders {
|
||||
[key: string]: Series.PathBuilder;
|
||||
}
|
||||
|
||||
let builders: PathBuilders | null = null;
|
||||
|
||||
/**
|
||||
* Get path builder based on draw style and interpolation
|
||||
*/
|
||||
@@ -207,23 +231,8 @@ function getPathBuilder(
|
||||
style: DrawStyle,
|
||||
lineInterpolation?: LineInterpolation,
|
||||
): Series.PathBuilder {
|
||||
const pathBuilders = uPlot.paths;
|
||||
|
||||
if (!builders) {
|
||||
const linearBuilder = pathBuilders.linear;
|
||||
const splineBuilder = pathBuilders.spline;
|
||||
const steppedBuilder = pathBuilders.stepped;
|
||||
|
||||
if (!linearBuilder || !splineBuilder || !steppedBuilder) {
|
||||
throw new Error('Required uPlot path builders are not available');
|
||||
}
|
||||
|
||||
builders = {
|
||||
linear: linearBuilder(),
|
||||
spline: splineBuilder(),
|
||||
stepBefore: steppedBuilder({ align: -1 }),
|
||||
stepAfter: steppedBuilder({ align: 1 }),
|
||||
};
|
||||
throw new Error('Required uPlot path builders are not available');
|
||||
}
|
||||
|
||||
if (style === DrawStyle.Line) {
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
import { getToolTipValue } from 'components/Graph/yAxisConfig';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { uPlotXAxisValuesFormat } from 'lib/uPlotLib/utils/constants';
|
||||
import type uPlot from 'uplot';
|
||||
|
||||
import type { AxisProps } from '../types';
|
||||
import { UPlotAxisBuilder } from '../UPlotAxisBuilder';
|
||||
|
||||
jest.mock('components/Graph/yAxisConfig', () => ({
|
||||
getToolTipValue: jest.fn(),
|
||||
}));
|
||||
|
||||
const createAxisProps = (overrides: Partial<AxisProps> = {}): AxisProps => ({
|
||||
scaleKey: 'x',
|
||||
label: 'Time',
|
||||
isDarkMode: false,
|
||||
show: true,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('UPlotAxisBuilder', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('builds basic axis config with defaults', () => {
|
||||
const builder = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
scaleKey: 'x',
|
||||
label: 'Time',
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.scale).toBe('x');
|
||||
expect(config.label).toBe('Time');
|
||||
expect(config.show).toBe(true);
|
||||
expect(config.side).toBe(2);
|
||||
expect(config.gap).toBe(5);
|
||||
|
||||
// Default grid and ticks are created
|
||||
expect(config.grid).toEqual({
|
||||
stroke: 'rgba(0,0,0,0.5)',
|
||||
width: 0.2,
|
||||
show: true,
|
||||
});
|
||||
expect(config.ticks).toEqual({
|
||||
width: 0.3,
|
||||
show: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('merges custom grid config over defaults and respects isDarkMode and isLogScale', () => {
|
||||
const builder = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
isDarkMode: true,
|
||||
isLogScale: true,
|
||||
grid: {
|
||||
width: 1,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.grid).toEqual({
|
||||
// stroke falls back to theme-based default when not provided
|
||||
stroke: 'rgba(231,233,237,0.3)',
|
||||
// provided width overrides default
|
||||
width: 1,
|
||||
// show falls back to default when not provided
|
||||
show: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses provided ticks config when present and falls back to defaults otherwise', () => {
|
||||
const customTicks = { width: 1, show: false };
|
||||
const withTicks = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
ticks: customTicks,
|
||||
}),
|
||||
);
|
||||
const withoutTicks = new UPlotAxisBuilder(createAxisProps());
|
||||
|
||||
expect(withTicks.getConfig().ticks).toBe(customTicks);
|
||||
expect(withoutTicks.getConfig().ticks).toEqual({
|
||||
width: 0.3,
|
||||
show: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses time-based X-axis values formatter for time-series like panels', () => {
|
||||
const builder = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
scaleKey: 'x',
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.values).toBe(uPlotXAxisValuesFormat);
|
||||
});
|
||||
|
||||
it('does not attach X-axis datetime formatter when panel type is not supported', () => {
|
||||
const builder = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
scaleKey: 'x',
|
||||
panelType: PANEL_TYPES.LIST, // not in PANEL_TYPES_WITH_X_AXIS_DATETIME_FORMAT
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.values).toBeUndefined();
|
||||
});
|
||||
|
||||
it('builds Y-axis values formatter that delegates to getToolTipValue', () => {
|
||||
const yBuilder = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
scaleKey: 'y',
|
||||
yAxisUnit: 'ms',
|
||||
decimalPrecision: 3,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = yBuilder.getConfig();
|
||||
expect(typeof config.values).toBe('function');
|
||||
|
||||
(getToolTipValue as jest.Mock).mockImplementation(
|
||||
(value: string, unit?: string, precision?: unknown) =>
|
||||
`formatted:${value}:${unit}:${precision}`,
|
||||
);
|
||||
|
||||
// Simulate uPlot calling the values formatter
|
||||
const valuesFn = (config.values as unknown) as (
|
||||
self: uPlot,
|
||||
vals: unknown[],
|
||||
) => string[];
|
||||
const result = valuesFn({} as uPlot, [1, null, 2, Number.NaN]);
|
||||
|
||||
expect(getToolTipValue).toHaveBeenCalledTimes(2);
|
||||
expect(getToolTipValue).toHaveBeenNthCalledWith(1, '1', 'ms', 3);
|
||||
expect(getToolTipValue).toHaveBeenNthCalledWith(2, '2', 'ms', 3);
|
||||
|
||||
// Null/NaN values should map to empty strings
|
||||
expect(result).toEqual(['formatted:1:ms:3', '', 'formatted:2:ms:3', '']);
|
||||
});
|
||||
|
||||
it('adds dynamic size calculator only for Y-axis when size is not provided', () => {
|
||||
const yBuilder = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
scaleKey: 'y',
|
||||
}),
|
||||
);
|
||||
const xBuilder = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
scaleKey: 'x',
|
||||
}),
|
||||
);
|
||||
|
||||
const yConfig = yBuilder.getConfig();
|
||||
const xConfig = xBuilder.getConfig();
|
||||
|
||||
expect(typeof yConfig.size).toBe('function');
|
||||
expect(xConfig.size).toBeUndefined();
|
||||
});
|
||||
|
||||
it('uses explicit size function when provided', () => {
|
||||
const sizeFn: uPlot.Axis.Size = jest.fn(() => 100) as uPlot.Axis.Size;
|
||||
|
||||
const builder = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
scaleKey: 'y',
|
||||
size: sizeFn,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
expect(config.size).toBe(sizeFn);
|
||||
});
|
||||
|
||||
it('builds stroke color based on stroke and isDarkMode', () => {
|
||||
const explicitStroke = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
stroke: '#ff0000',
|
||||
}),
|
||||
);
|
||||
const darkStroke = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
stroke: undefined,
|
||||
isDarkMode: true,
|
||||
}),
|
||||
);
|
||||
const lightStroke = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
stroke: undefined,
|
||||
isDarkMode: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(explicitStroke.getConfig().stroke).toBe('#ff0000');
|
||||
expect(darkStroke.getConfig().stroke).toBe('white');
|
||||
expect(lightStroke.getConfig().stroke).toBe('black');
|
||||
});
|
||||
|
||||
it('uses explicit values formatter when provided', () => {
|
||||
const customValues: uPlot.Axis.Values = jest.fn(() => ['a', 'b', 'c']);
|
||||
|
||||
const builder = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
scaleKey: 'y',
|
||||
values: customValues,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.values).toBe(customValues);
|
||||
});
|
||||
|
||||
it('returns undefined values for scaleKey neither x nor y', () => {
|
||||
const builder = new UPlotAxisBuilder(createAxisProps({ scaleKey: 'custom' }));
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.values).toBeUndefined();
|
||||
});
|
||||
|
||||
it('includes space in config when provided', () => {
|
||||
const builder = new UPlotAxisBuilder(
|
||||
createAxisProps({ scaleKey: 'y', space: 50 }),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.space).toBe(50);
|
||||
});
|
||||
|
||||
it('includes PANEL_TYPES.BAR and PANEL_TYPES.PIE in X-axis datetime formatter', () => {
|
||||
const barBuilder = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
scaleKey: 'x',
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
}),
|
||||
);
|
||||
const pieBuilder = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
scaleKey: 'x',
|
||||
panelType: PANEL_TYPES.PIE,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(barBuilder.getConfig().values).toBe(uPlotXAxisValuesFormat);
|
||||
expect(pieBuilder.getConfig().values).toBe(uPlotXAxisValuesFormat);
|
||||
});
|
||||
|
||||
it('invokes Y-axis size calculator and delegates to getExistingAxisSize when cycleNum > 1', () => {
|
||||
const builder = new UPlotAxisBuilder(createAxisProps({ scaleKey: 'y' }));
|
||||
|
||||
const config = builder.getConfig();
|
||||
const sizeFn = config.size;
|
||||
expect(typeof sizeFn).toBe('function');
|
||||
|
||||
const mockAxis = {
|
||||
_size: 80,
|
||||
ticks: { size: 10 },
|
||||
font: ['12px sans-serif'],
|
||||
};
|
||||
const mockSelf = ({
|
||||
axes: [mockAxis],
|
||||
ctx: { measureText: jest.fn(() => ({ width: 60 })), font: '' },
|
||||
} as unknown) as uPlot;
|
||||
|
||||
const result = (sizeFn as (
|
||||
s: uPlot,
|
||||
v: string[],
|
||||
a: number,
|
||||
c: number,
|
||||
) => number)(
|
||||
mockSelf,
|
||||
['100', '200'],
|
||||
0,
|
||||
2, // cycleNum > 1
|
||||
);
|
||||
|
||||
expect(result).toBe(80);
|
||||
});
|
||||
|
||||
it('invokes Y-axis size calculator and computes from text width when cycleNum <= 1', () => {
|
||||
const builder = new UPlotAxisBuilder(
|
||||
createAxisProps({ scaleKey: 'y', gap: 8 }),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
const sizeFn = config.size;
|
||||
expect(typeof sizeFn).toBe('function');
|
||||
|
||||
const mockAxis = {
|
||||
ticks: { size: 12 },
|
||||
font: ['12px sans-serif'],
|
||||
};
|
||||
const measureText = jest.fn(() => ({ width: 48 }));
|
||||
const mockSelf = ({
|
||||
axes: [mockAxis],
|
||||
ctx: {
|
||||
measureText,
|
||||
get font() {
|
||||
return '';
|
||||
},
|
||||
set font(_v: string) {
|
||||
/* noop */
|
||||
},
|
||||
},
|
||||
} as unknown) as uPlot;
|
||||
|
||||
const result = (sizeFn as (
|
||||
s: uPlot,
|
||||
v: string[],
|
||||
a: number,
|
||||
c: number,
|
||||
) => number)(
|
||||
mockSelf,
|
||||
['10', '2000ms'],
|
||||
0,
|
||||
0, // cycleNum <= 1
|
||||
);
|
||||
|
||||
expect(measureText).toHaveBeenCalledWith('2000ms');
|
||||
expect(result).toBeGreaterThanOrEqual(12 + 8);
|
||||
});
|
||||
|
||||
it('merge updates axis props', () => {
|
||||
const builder = new UPlotAxisBuilder(
|
||||
createAxisProps({ scaleKey: 'y', label: 'Original' }),
|
||||
);
|
||||
|
||||
builder.merge({ label: 'Merged', yAxisUnit: 'bytes' });
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.label).toBe('Merged');
|
||||
expect(config.values).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,317 @@
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import type { SeriesProps } from '../types';
|
||||
import { DrawStyle, SelectionPreferencesSource } from '../types';
|
||||
import { UPlotConfigBuilder } from '../UPlotConfigBuilder';
|
||||
|
||||
// Mock only the real boundary that hits localStorage
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils',
|
||||
() => ({
|
||||
getStoredSeriesVisibility: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
const getStoredSeriesVisibilityMock = jest.requireMock(
|
||||
'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils',
|
||||
) as {
|
||||
getStoredSeriesVisibility: jest.Mock;
|
||||
};
|
||||
|
||||
describe('UPlotConfigBuilder', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const createSeriesProps = (
|
||||
overrides: Partial<SeriesProps> = {},
|
||||
): SeriesProps => ({
|
||||
scaleKey: 'y',
|
||||
label: 'Requests',
|
||||
colorMapping: {},
|
||||
drawStyle: DrawStyle.Line,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it('returns correct save selection preference flag from constructor args', () => {
|
||||
const builder = new UPlotConfigBuilder({
|
||||
shouldSaveSelectionPreference: true,
|
||||
});
|
||||
|
||||
expect(builder.getShouldSaveSelectionPreference()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns widgetId from constructor args', () => {
|
||||
const builder = new UPlotConfigBuilder({ widgetId: 'widget-123' });
|
||||
|
||||
expect(builder.getWidgetId()).toBe('widget-123');
|
||||
});
|
||||
|
||||
it('sets tzDate from constructor and includes it in config', () => {
|
||||
const tzDate = (ts: number): Date => new Date(ts);
|
||||
const builder = new UPlotConfigBuilder({ tzDate });
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.tzDate).toBe(tzDate);
|
||||
});
|
||||
|
||||
it('does not call onDragSelect for click without drag (width === 0)', () => {
|
||||
const onDragSelect = jest.fn();
|
||||
const builder = new UPlotConfigBuilder({ onDragSelect });
|
||||
|
||||
const config = builder.getConfig();
|
||||
const setSelectHooks = config.hooks?.setSelect ?? [];
|
||||
expect(setSelectHooks.length).toBe(1);
|
||||
|
||||
const uplotInstance = ({
|
||||
select: { left: 10, width: 0 },
|
||||
posToVal: jest.fn(),
|
||||
} as unknown) as uPlot;
|
||||
|
||||
// Simulate uPlot calling the hook
|
||||
const setSelectHook = setSelectHooks[0];
|
||||
setSelectHook!(uplotInstance);
|
||||
|
||||
expect(onDragSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onDragSelect with start and end times in milliseconds for a drag selection', () => {
|
||||
const onDragSelect = jest.fn();
|
||||
const builder = new UPlotConfigBuilder({ onDragSelect });
|
||||
|
||||
const config = builder.getConfig();
|
||||
const setSelectHooks = config.hooks?.setSelect ?? [];
|
||||
expect(setSelectHooks.length).toBe(1);
|
||||
|
||||
const posToVal = jest
|
||||
.fn()
|
||||
// left position
|
||||
.mockReturnValueOnce(100)
|
||||
// left + width
|
||||
.mockReturnValueOnce(110);
|
||||
|
||||
const uplotInstance = ({
|
||||
select: { left: 50, width: 20 },
|
||||
posToVal,
|
||||
} as unknown) as uPlot;
|
||||
|
||||
const setSelectHook = setSelectHooks[0];
|
||||
setSelectHook!(uplotInstance);
|
||||
|
||||
expect(onDragSelect).toHaveBeenCalledTimes(1);
|
||||
// 100 and 110 seconds converted to milliseconds
|
||||
expect(onDragSelect).toHaveBeenCalledWith(100_000, 110_000);
|
||||
});
|
||||
|
||||
it('adds and removes hooks via addHook, and exposes them through getConfig', () => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
const drawHook = jest.fn();
|
||||
|
||||
const remove = builder.addHook('draw', drawHook as uPlot.Hooks.Defs['draw']);
|
||||
|
||||
let config = builder.getConfig();
|
||||
expect(config.hooks?.draw).toContain(drawHook);
|
||||
|
||||
// Remove and ensure it no longer appears in config
|
||||
remove();
|
||||
config = builder.getConfig();
|
||||
expect(config.hooks?.draw ?? []).not.toContain(drawHook);
|
||||
});
|
||||
|
||||
it('adds axes, scales, and series and wires them into the final config', () => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
|
||||
// Add axis and scale
|
||||
builder.addAxis({ scaleKey: 'y', label: 'Requests' });
|
||||
builder.addScale({ scaleKey: 'y' });
|
||||
|
||||
// Add two series – legend indices should start from 1 (0 is the timestamp series)
|
||||
builder.addSeries(createSeriesProps({ label: 'Requests' }));
|
||||
builder.addSeries(createSeriesProps({ label: 'Errors' }));
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
// Axes
|
||||
expect(config.axes).toHaveLength(1);
|
||||
expect(config.axes?.[0].scale).toBe('y');
|
||||
|
||||
// Scales are returned as an object keyed by scaleKey
|
||||
expect(config.scales).toBeDefined();
|
||||
expect(Object.keys(config.scales ?? {})).toContain('y');
|
||||
|
||||
// Series: base timestamp + 2 data series
|
||||
expect(config.series).toHaveLength(3);
|
||||
// Base series (index 0) has a value formatter that returns empty string
|
||||
const baseSeries = config.series?.[0] as { value?: () => string };
|
||||
expect(typeof baseSeries?.value).toBe('function');
|
||||
expect(baseSeries?.value?.()).toBe('');
|
||||
|
||||
// Legend items align with series and carry label and color from series config
|
||||
const legendItems = builder.getLegendItems();
|
||||
expect(Object.keys(legendItems)).toEqual(['1', '2']);
|
||||
expect(legendItems[1].seriesIndex).toBe(1);
|
||||
expect(legendItems[1].label).toBe('Requests');
|
||||
expect(legendItems[2].label).toBe('Errors');
|
||||
});
|
||||
|
||||
it('merges axis when addAxis is called twice with same scaleKey', () => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
|
||||
builder.addAxis({ scaleKey: 'y', label: 'Requests' });
|
||||
builder.addAxis({ scaleKey: 'y', label: 'Updated Label', show: false });
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.axes).toHaveLength(1);
|
||||
expect(config.axes?.[0].label).toBe('Updated Label');
|
||||
expect(config.axes?.[0].show).toBe(false);
|
||||
});
|
||||
|
||||
it('merges scale when addScale is called twice with same scaleKey', () => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
|
||||
builder.addScale({ scaleKey: 'y', min: 0 });
|
||||
builder.addScale({ scaleKey: 'y', max: 100 });
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
// Only one scale entry for 'y' (merge path used, no duplicate added)
|
||||
expect(config.scales).toBeDefined();
|
||||
const scales = config.scales ?? {};
|
||||
expect(Object.keys(scales)).toEqual(['y']);
|
||||
expect(scales.y?.range).toBeDefined();
|
||||
});
|
||||
|
||||
it('restores visibility state from localStorage when selectionPreferencesSource is LOCAL_STORAGE', () => {
|
||||
const visibilityMap = new Map<string, boolean>([
|
||||
['Requests', true],
|
||||
['Errors', false],
|
||||
]);
|
||||
|
||||
getStoredSeriesVisibilityMock.getStoredSeriesVisibility.mockReturnValue(
|
||||
visibilityMap,
|
||||
);
|
||||
|
||||
const builder = new UPlotConfigBuilder({
|
||||
widgetId: 'widget-1',
|
||||
selectionPreferencesSource: SelectionPreferencesSource.LOCAL_STORAGE,
|
||||
});
|
||||
|
||||
builder.addSeries(createSeriesProps({ label: 'Requests' }));
|
||||
builder.addSeries(createSeriesProps({ label: 'Errors' }));
|
||||
|
||||
const legendItems = builder.getLegendItems();
|
||||
|
||||
// When any series is hidden, legend visibility is driven by the stored map
|
||||
expect(legendItems[1].show).toBe(true);
|
||||
expect(legendItems[2].show).toBe(false);
|
||||
|
||||
const config = builder.getConfig();
|
||||
const [, firstSeries, secondSeries] = config.series ?? [];
|
||||
|
||||
expect(firstSeries?.show).toBe(true);
|
||||
expect(secondSeries?.show).toBe(false);
|
||||
});
|
||||
|
||||
it('does not attempt to read stored visibility when using in-memory preferences', () => {
|
||||
const builder = new UPlotConfigBuilder({
|
||||
widgetId: 'widget-1',
|
||||
selectionPreferencesSource: SelectionPreferencesSource.IN_MEMORY,
|
||||
});
|
||||
|
||||
builder.addSeries(createSeriesProps({ label: 'Requests' }));
|
||||
|
||||
builder.getLegendItems();
|
||||
builder.getConfig();
|
||||
|
||||
expect(
|
||||
getStoredSeriesVisibilityMock.getStoredSeriesVisibility,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('adds thresholds only once per scale key', () => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
|
||||
const thresholdsOptions = {
|
||||
scaleKey: 'y',
|
||||
thresholds: [{ thresholdValue: 100 }],
|
||||
};
|
||||
|
||||
builder.addThresholds(thresholdsOptions);
|
||||
builder.addThresholds(thresholdsOptions);
|
||||
|
||||
const config = builder.getConfig();
|
||||
const drawHooks = config.hooks?.draw ?? [];
|
||||
|
||||
// Only a single draw hook should be registered for the same scaleKey
|
||||
expect(drawHooks.length).toBe(1);
|
||||
});
|
||||
|
||||
it('merges cursor configuration with defaults instead of replacing them', () => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
|
||||
builder.setCursor({
|
||||
drag: { setScale: false },
|
||||
});
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.cursor?.drag?.setScale).toBe(false);
|
||||
// Points configuration from DEFAULT_CURSOR_CONFIG should still be present
|
||||
expect(config.cursor?.points).toBeDefined();
|
||||
});
|
||||
|
||||
it('adds plugins and includes them in config', () => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
const plugin: uPlot.Plugin = {
|
||||
opts: (): void => {},
|
||||
hooks: {},
|
||||
};
|
||||
|
||||
builder.addPlugin(plugin);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.plugins).toContain(plugin);
|
||||
});
|
||||
|
||||
it('sets padding, legend, focus, select, tzDate, bands and includes them in config', () => {
|
||||
const tzDate = (ts: number): Date => new Date(ts);
|
||||
const builder = new UPlotConfigBuilder();
|
||||
|
||||
const bands: uPlot.Band[] = [{ series: [1, 2], fill: (): string => '#000' }];
|
||||
|
||||
builder.setBands(bands);
|
||||
builder.setPadding([10, 20, 30, 40]);
|
||||
builder.setLegend({ show: true, live: true });
|
||||
builder.setFocus({ alpha: 0.5 });
|
||||
builder.setSelect({ left: 0, width: 0, top: 0, height: 0 });
|
||||
builder.setTzDate(tzDate);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.bands).toEqual(bands);
|
||||
expect(config.padding).toEqual([10, 20, 30, 40]);
|
||||
expect(config.legend).toEqual({ show: true, live: true });
|
||||
expect(config.focus).toEqual({ alpha: 0.5 });
|
||||
expect(config.select).toEqual({ left: 0, width: 0, top: 0, height: 0 });
|
||||
expect(config.tzDate).toBe(tzDate);
|
||||
});
|
||||
|
||||
it('does not include plugins when none added', () => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.plugins).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not include bands when empty', () => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.bands).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,236 @@
|
||||
import type uPlot from 'uplot';
|
||||
|
||||
import * as scaleUtils from '../../utils/scale';
|
||||
import type { ScaleProps } from '../types';
|
||||
import { DistributionType } from '../types';
|
||||
import { UPlotScaleBuilder } from '../UPlotScaleBuilder';
|
||||
|
||||
const createScaleProps = (overrides: Partial<ScaleProps> = {}): ScaleProps => ({
|
||||
scaleKey: 'y',
|
||||
time: false,
|
||||
auto: undefined,
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
softMin: undefined,
|
||||
softMax: undefined,
|
||||
distribution: DistributionType.Linear,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('UPlotScaleBuilder', () => {
|
||||
const getFallbackMinMaxSpy = jest.spyOn(
|
||||
scaleUtils,
|
||||
'getFallbackMinMaxTimeStamp',
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('initializes softMin/softMax correctly when both are 0 (treated as unset)', () => {
|
||||
const builder = new UPlotScaleBuilder(
|
||||
createScaleProps({
|
||||
softMin: 0,
|
||||
softMax: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
// Non-time scale so config path uses thresholds pipeline; we just care that
|
||||
// adjustSoftLimitsWithThresholds receives null soft limits instead of 0/0.
|
||||
const adjustSpy = jest.spyOn(scaleUtils, 'adjustSoftLimitsWithThresholds');
|
||||
|
||||
builder.getConfig();
|
||||
|
||||
expect(adjustSpy).toHaveBeenCalledWith(null, null, undefined, undefined);
|
||||
});
|
||||
|
||||
it('handles time scales using explicit min/max and rounds max down to the previous minute', () => {
|
||||
const min = 1_700_000_000; // seconds
|
||||
const max = 1_700_000_600; // seconds
|
||||
|
||||
const builder = new UPlotScaleBuilder(
|
||||
createScaleProps({
|
||||
scaleKey: 'x',
|
||||
time: true,
|
||||
min,
|
||||
max,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
const xScale = config.x;
|
||||
|
||||
expect(xScale.time).toBe(true);
|
||||
expect(xScale.auto).toBe(false);
|
||||
expect(Array.isArray(xScale.range)).toBe(true);
|
||||
|
||||
const [resolvedMin, resolvedMax] = xScale.range as [number, number];
|
||||
|
||||
// min is passed through
|
||||
expect(resolvedMin).toBe(min);
|
||||
|
||||
// max is coerced to "endTime - 1 minute" and rounded down to minute precision
|
||||
const oneMinuteAgoTimestamp = (max - 60) * 1000;
|
||||
const currentDate = new Date(oneMinuteAgoTimestamp);
|
||||
currentDate.setSeconds(0);
|
||||
currentDate.setMilliseconds(0);
|
||||
const expectedMax = Math.floor(currentDate.getTime() / 1000);
|
||||
|
||||
expect(resolvedMax).toBe(expectedMax);
|
||||
});
|
||||
|
||||
it('falls back to getFallbackMinMaxTimeStamp when time scale has no min/max', () => {
|
||||
getFallbackMinMaxSpy.mockReturnValue({
|
||||
fallbackMin: 100,
|
||||
fallbackMax: 200,
|
||||
});
|
||||
|
||||
const builder = new UPlotScaleBuilder(
|
||||
createScaleProps({
|
||||
scaleKey: 'x',
|
||||
time: true,
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
const [resolvedMin, resolvedMax] = config.x.range as [number, number];
|
||||
|
||||
expect(getFallbackMinMaxSpy).toHaveBeenCalled();
|
||||
expect(resolvedMin).toBe(100);
|
||||
// max is aligned to "fallbackMax - 60 seconds" minute boundary
|
||||
expect(resolvedMax).toBeLessThanOrEqual(200);
|
||||
expect(resolvedMax).toBeGreaterThan(100);
|
||||
});
|
||||
|
||||
it('pipes limits through soft-limit adjustment and log-scale normalization before range config', () => {
|
||||
const adjustSpy = jest.spyOn(scaleUtils, 'adjustSoftLimitsWithThresholds');
|
||||
const normalizeSpy = jest.spyOn(scaleUtils, 'normalizeLogScaleLimits');
|
||||
const getRangeConfigSpy = jest.spyOn(scaleUtils, 'getRangeConfig');
|
||||
|
||||
const thresholds = {
|
||||
scaleKey: 'y',
|
||||
thresholds: [{ thresholdValue: 10 }],
|
||||
yAxisUnit: 'ms',
|
||||
};
|
||||
|
||||
const builder = new UPlotScaleBuilder(
|
||||
createScaleProps({
|
||||
softMin: 1,
|
||||
softMax: 5,
|
||||
min: 0,
|
||||
max: 100,
|
||||
distribution: DistributionType.Logarithmic,
|
||||
thresholds,
|
||||
logBase: 2,
|
||||
padMinBy: 0.1,
|
||||
padMaxBy: 0.2,
|
||||
}),
|
||||
);
|
||||
|
||||
builder.getConfig();
|
||||
|
||||
expect(adjustSpy).toHaveBeenCalledWith(1, 5, thresholds.thresholds, 'ms');
|
||||
expect(normalizeSpy).toHaveBeenCalledWith({
|
||||
distr: DistributionType.Logarithmic,
|
||||
logBase: 2,
|
||||
limits: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
softMin: expect.anything(),
|
||||
softMax: expect.anything(),
|
||||
},
|
||||
});
|
||||
expect(getRangeConfigSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('computes distribution config for non-time scales and wires range function when range is not provided', () => {
|
||||
const createRangeFnSpy = jest.spyOn(scaleUtils, 'createRangeFunction');
|
||||
|
||||
const builder = new UPlotScaleBuilder(
|
||||
createScaleProps({
|
||||
scaleKey: 'y',
|
||||
time: false,
|
||||
distribution: DistributionType.Linear,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
const yScale = config.y;
|
||||
|
||||
expect(createRangeFnSpy).toHaveBeenCalled();
|
||||
|
||||
// range should be a function when not provided explicitly
|
||||
expect(typeof yScale.range).toBe('function');
|
||||
|
||||
// distribution config should be applied
|
||||
expect(yScale.distr).toBeDefined();
|
||||
expect(yScale.log).toBeDefined();
|
||||
});
|
||||
|
||||
it('respects explicit range function when provided on props', () => {
|
||||
const explicitRange: uPlot.Scale.Range = jest.fn(() => [
|
||||
0,
|
||||
10,
|
||||
]) as uPlot.Scale.Range;
|
||||
|
||||
const builder = new UPlotScaleBuilder(
|
||||
createScaleProps({
|
||||
scaleKey: 'y',
|
||||
range: explicitRange,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
const yScale = config.y;
|
||||
|
||||
expect(yScale.range).toBe(explicitRange);
|
||||
});
|
||||
|
||||
it('derives auto flag when not explicitly provided, based on hasFixedRange and time', () => {
|
||||
const getRangeConfigSpy = jest.spyOn(scaleUtils, 'getRangeConfig');
|
||||
|
||||
const builder = new UPlotScaleBuilder(
|
||||
createScaleProps({
|
||||
min: 0,
|
||||
max: 100,
|
||||
time: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
const yScale = config.y;
|
||||
|
||||
expect(getRangeConfigSpy).toHaveBeenCalled();
|
||||
// For non-time scale with fixed min/max, hasFixedRange is true → auto should remain false
|
||||
expect(yScale.auto).toBe(false);
|
||||
});
|
||||
|
||||
it('merge updates internal min/max/soft limits while preserving other props', () => {
|
||||
const builder = new UPlotScaleBuilder(
|
||||
createScaleProps({
|
||||
scaleKey: 'y',
|
||||
min: 0,
|
||||
max: 10,
|
||||
softMin: 1,
|
||||
softMax: 9,
|
||||
time: false,
|
||||
}),
|
||||
);
|
||||
|
||||
builder.merge({
|
||||
min: 2,
|
||||
softMax: undefined,
|
||||
});
|
||||
|
||||
expect(builder.props.min).toBe(2);
|
||||
expect(builder.props.softMax).toBe(undefined);
|
||||
expect(builder.props.max).toBe(10);
|
||||
expect(builder.props.softMin).toBe(1);
|
||||
expect(builder.props.time).toBe(false);
|
||||
expect(builder.props.scaleKey).toBe('y');
|
||||
expect(builder.props.distribution).toBe(DistributionType.Linear);
|
||||
expect(builder.props.thresholds).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,300 @@
|
||||
import { themeColors } from 'constants/theme';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import type { SeriesProps } from '../types';
|
||||
import {
|
||||
DrawStyle,
|
||||
LineInterpolation,
|
||||
LineStyle,
|
||||
VisibilityMode,
|
||||
} from '../types';
|
||||
import { UPlotSeriesBuilder } from '../UPlotSeriesBuilder';
|
||||
|
||||
const createBaseProps = (
|
||||
overrides: Partial<SeriesProps> = {},
|
||||
): SeriesProps => ({
|
||||
scaleKey: 'y',
|
||||
label: 'Requests',
|
||||
colorMapping: {},
|
||||
drawStyle: DrawStyle.Line,
|
||||
isDarkMode: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
interface MockPath extends uPlot.Series.Paths {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
describe('UPlotSeriesBuilder', () => {
|
||||
it('maps basic props into uPlot series config', () => {
|
||||
const builder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
label: 'Latency',
|
||||
spanGaps: true,
|
||||
show: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.scale).toBe('y');
|
||||
expect(config.label).toBe('Latency');
|
||||
expect(config.spanGaps).toBe(true);
|
||||
expect(config.show).toBe(false);
|
||||
expect(config.pxAlign).toBe(true);
|
||||
expect(typeof config.value).toBe('function');
|
||||
});
|
||||
|
||||
it('uses explicit lineColor when provided, regardless of mapping', () => {
|
||||
const builder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
lineColor: '#ff00ff',
|
||||
colorMapping: { Requests: '#00ff00' },
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.stroke).toBe('#ff00ff');
|
||||
});
|
||||
|
||||
it('falls back to theme colors when no label is provided', () => {
|
||||
const darkBuilder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
label: undefined,
|
||||
isDarkMode: true,
|
||||
lineColor: undefined,
|
||||
}),
|
||||
);
|
||||
const lightBuilder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
label: undefined,
|
||||
isDarkMode: false,
|
||||
lineColor: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
const darkConfig = darkBuilder.getConfig();
|
||||
const lightConfig = lightBuilder.getConfig();
|
||||
|
||||
expect(darkConfig.stroke).toBe(themeColors.white);
|
||||
expect(lightConfig.stroke).toBe(themeColors.black);
|
||||
});
|
||||
|
||||
it('uses colorMapping when available and no explicit lineColor is provided', () => {
|
||||
const builder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
label: 'Requests',
|
||||
colorMapping: { Requests: '#123456' },
|
||||
lineColor: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.stroke).toBe('#123456');
|
||||
});
|
||||
|
||||
it('passes through a custom pathBuilder when provided', () => {
|
||||
const customPaths = (jest.fn() as unknown) as uPlot.Series.PathBuilder;
|
||||
|
||||
const builder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
pathBuilder: customPaths,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.paths).toBe(customPaths);
|
||||
});
|
||||
|
||||
it('does not build line paths when drawStyle is Points, but still renders points', () => {
|
||||
const builder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
drawStyle: DrawStyle.Points,
|
||||
pointSize: 4,
|
||||
lineWidth: 2,
|
||||
lineColor: '#aa00aa',
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(typeof config.paths).toBe('function');
|
||||
expect(config.paths && config.paths({} as uPlot, 1, 0, 10)).toBeNull();
|
||||
|
||||
expect(config.points).toBeDefined();
|
||||
expect(config.points?.stroke).toBe('#aa00aa');
|
||||
expect(config.points?.fill).toBe('#aa00aa');
|
||||
expect(config.points?.show).toBe(true);
|
||||
expect(config.points?.size).toBe(4);
|
||||
});
|
||||
|
||||
it('derives point size based on lineWidth and pointSize', () => {
|
||||
const smallPointsBuilder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
lineWidth: 4,
|
||||
pointSize: 2,
|
||||
}),
|
||||
);
|
||||
const largePointsBuilder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
lineWidth: 2,
|
||||
pointSize: 4,
|
||||
}),
|
||||
);
|
||||
|
||||
const smallConfig = smallPointsBuilder.getConfig();
|
||||
const largeConfig = largePointsBuilder.getConfig();
|
||||
|
||||
expect(smallConfig.points?.size).toBeUndefined();
|
||||
expect(largeConfig.points?.size).toBe(4);
|
||||
});
|
||||
|
||||
it('uses pointsBuilder when provided instead of default visibility logic', () => {
|
||||
const pointsBuilder: uPlot.Series.Points.Show = jest.fn(
|
||||
() => true,
|
||||
) as uPlot.Series.Points.Show;
|
||||
|
||||
const builder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
pointsBuilder,
|
||||
drawStyle: DrawStyle.Line,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.points?.show).toBe(pointsBuilder);
|
||||
});
|
||||
|
||||
it('respects VisibilityMode for point visibility when no custom pointsBuilder is given', () => {
|
||||
const neverPointsBuilder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
drawStyle: DrawStyle.Line,
|
||||
showPoints: VisibilityMode.Never,
|
||||
}),
|
||||
);
|
||||
const alwaysPointsBuilder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
drawStyle: DrawStyle.Line,
|
||||
showPoints: VisibilityMode.Always,
|
||||
}),
|
||||
);
|
||||
|
||||
const neverConfig = neverPointsBuilder.getConfig();
|
||||
const alwaysConfig = alwaysPointsBuilder.getConfig();
|
||||
|
||||
expect(neverConfig.points?.show).toBe(false);
|
||||
expect(alwaysConfig.points?.show).toBe(true);
|
||||
});
|
||||
|
||||
it('applies LineStyle.Dashed and lineCap to line config', () => {
|
||||
const builder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
lineStyle: LineStyle.Dashed,
|
||||
lineCap: 'round' as CanvasLineCap,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.dash).toEqual([10, 10]);
|
||||
expect(config.cap).toBe('round');
|
||||
});
|
||||
|
||||
it('builds default paths for Line drawStyle and invokes the path builder', () => {
|
||||
const builder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
drawStyle: DrawStyle.Line,
|
||||
lineInterpolation: LineInterpolation.Linear,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
const result = config.paths?.({} as uPlot, 1, 0, 10);
|
||||
expect((result as MockPath).name).toBe('linear');
|
||||
});
|
||||
|
||||
it('uses StepBefore and StepAfter interpolation for line paths', () => {
|
||||
const stepBeforeBuilder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
drawStyle: DrawStyle.Line,
|
||||
lineInterpolation: LineInterpolation.StepBefore,
|
||||
}),
|
||||
);
|
||||
const stepAfterBuilder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
drawStyle: DrawStyle.Line,
|
||||
lineInterpolation: LineInterpolation.StepAfter,
|
||||
}),
|
||||
);
|
||||
|
||||
const stepBeforeConfig = stepBeforeBuilder.getConfig();
|
||||
const stepAfterConfig = stepAfterBuilder.getConfig();
|
||||
const stepBeforePath = stepBeforeConfig.paths?.({} as uPlot, 1, 0, 5);
|
||||
const stepAfterPath = stepAfterConfig.paths?.({} as uPlot, 1, 0, 5);
|
||||
expect((stepBeforePath as MockPath).name).toBe('stepped-(-1)');
|
||||
expect((stepAfterPath as MockPath).name).toBe('stepped-(1)');
|
||||
});
|
||||
|
||||
it('defaults to spline interpolation when lineInterpolation is Spline or undefined', () => {
|
||||
const splineBuilder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
drawStyle: DrawStyle.Line,
|
||||
lineInterpolation: LineInterpolation.Spline,
|
||||
}),
|
||||
);
|
||||
const defaultBuilder = new UPlotSeriesBuilder(
|
||||
createBaseProps({ drawStyle: DrawStyle.Line }),
|
||||
);
|
||||
|
||||
const splineConfig = splineBuilder.getConfig();
|
||||
const defaultConfig = defaultBuilder.getConfig();
|
||||
|
||||
const splinePath = splineConfig.paths?.({} as uPlot, 1, 0, 10);
|
||||
const defaultPath = defaultConfig.paths?.({} as uPlot, 1, 0, 10);
|
||||
|
||||
expect((splinePath as MockPath).name).toBe('spline');
|
||||
expect((defaultPath as MockPath).name).toBe('spline');
|
||||
});
|
||||
|
||||
it('preserves spanGaps true when provided as boolean', () => {
|
||||
const builder = new UPlotSeriesBuilder(createBaseProps({ spanGaps: true }));
|
||||
|
||||
const config = builder.getConfig();
|
||||
expect(config.spanGaps).toBe(true);
|
||||
});
|
||||
|
||||
it('uses generateColor when label has no colorMapping and no lineColor', () => {
|
||||
const builder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
label: 'CustomSeries',
|
||||
colorMapping: {},
|
||||
lineColor: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
expect(config.stroke).toBe('#E64A3C');
|
||||
});
|
||||
|
||||
it('passes through pointsFilter when provided', () => {
|
||||
const pointsFilter: uPlot.Series.Points.Filter = jest.fn(
|
||||
(_self, _seriesIdx, _show) => null,
|
||||
);
|
||||
|
||||
const builder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
pointsFilter,
|
||||
drawStyle: DrawStyle.Line,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.points?.filter).toBe(pointsFilter);
|
||||
});
|
||||
});
|
||||
@@ -1,484 +0,0 @@
|
||||
import React from 'react';
|
||||
import { act, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render } from 'tests/test-utils';
|
||||
import type uPlot from 'uplot';
|
||||
|
||||
import { TooltipRenderArgs } from '../../components/types';
|
||||
import { UPlotConfigBuilder } from '../../config/UPlotConfigBuilder';
|
||||
import TooltipPlugin from '../TooltipPlugin/TooltipPlugin';
|
||||
import { DashboardCursorSync } from '../TooltipPlugin/types';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type HookHandler = (...args: unknown[]) => void;
|
||||
|
||||
class TestConfigBuilder extends UPlotConfigBuilder {
|
||||
public registeredHooks: { type: string; handler: HookHandler }[] = [];
|
||||
|
||||
public removeCallbacks: jest.Mock[] = [];
|
||||
|
||||
// Override addHook so we can:
|
||||
// - capture handlers by hook name for tests
|
||||
// - return removable jest mocks to assert cleanup
|
||||
public addHook<T extends keyof uPlot.Hooks.Defs>(
|
||||
type: T,
|
||||
hook: uPlot.Hooks.Defs[T],
|
||||
): () => void {
|
||||
this.registeredHooks.push({
|
||||
type: String(type),
|
||||
handler: hook as HookHandler,
|
||||
});
|
||||
const remove = jest.fn();
|
||||
this.removeCallbacks.push(remove);
|
||||
return remove;
|
||||
}
|
||||
}
|
||||
|
||||
type ConfigMock = TestConfigBuilder;
|
||||
|
||||
function createConfigMock(): ConfigMock {
|
||||
return new TestConfigBuilder();
|
||||
}
|
||||
|
||||
function getHandler(config: ConfigMock, hookName: string): HookHandler {
|
||||
const entry = config.registeredHooks.find((h) => h.type === hookName);
|
||||
if (!entry) {
|
||||
throw new Error(`Hook "${hookName}" was not registered on config`);
|
||||
}
|
||||
return entry.handler;
|
||||
}
|
||||
|
||||
function createFakePlot(): {
|
||||
over: HTMLDivElement;
|
||||
setCursor: jest.Mock<void, [uPlot.Cursor]>;
|
||||
cursor: { event: Record<string, unknown> };
|
||||
} {
|
||||
return {
|
||||
over: document.createElement('div'),
|
||||
setCursor: jest.fn(),
|
||||
cursor: { event: {} },
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('TooltipPlugin', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
|
||||
(callback as FrameRequestCallback)(0);
|
||||
return 0;
|
||||
});
|
||||
jest
|
||||
.spyOn(window, 'cancelAnimationFrame')
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
.mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
/**
|
||||
* Shorthand: render the plugin, initialise a fake plot, and trigger a
|
||||
* series focus so the tooltip becomes visible. Returns the fake plot
|
||||
* instance for further interaction (e.g. clicking the overlay).
|
||||
*/
|
||||
function renderAndActivateHover(
|
||||
config: ConfigMock,
|
||||
renderFn: (
|
||||
args: TooltipRenderArgs,
|
||||
) => React.ReactNode = (): React.ReactNode =>
|
||||
React.createElement('div', null, 'tooltip-body'),
|
||||
extraProps: Partial<React.ComponentProps<typeof TooltipPlugin>> = {},
|
||||
): ReturnType<typeof createFakePlot> {
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config,
|
||||
render: renderFn,
|
||||
syncMode: DashboardCursorSync.None,
|
||||
...extraProps,
|
||||
}),
|
||||
);
|
||||
|
||||
const fakePlot = createFakePlot();
|
||||
const initHandler = getHandler(config, 'init');
|
||||
const setSeriesHandler = getHandler(config, 'setSeries');
|
||||
|
||||
act(() => {
|
||||
initHandler(fakePlot);
|
||||
setSeriesHandler(fakePlot, 1, { focus: true });
|
||||
});
|
||||
|
||||
return fakePlot;
|
||||
}
|
||||
|
||||
// ---- Initial state --------------------------------------------------------
|
||||
|
||||
describe('before any interaction', () => {
|
||||
it('does not render anything when there is no active hover', () => {
|
||||
const config = createConfigMock();
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config,
|
||||
render: () => React.createElement('div', null, 'tooltip-body'),
|
||||
syncMode: DashboardCursorSync.None,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
});
|
||||
|
||||
it('registers all required uPlot hooks on mount', () => {
|
||||
const config = createConfigMock();
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config,
|
||||
render: () => null,
|
||||
syncMode: DashboardCursorSync.None,
|
||||
}),
|
||||
);
|
||||
|
||||
const registered = config.registeredHooks.map((h) => h.type);
|
||||
expect(registered).toContain('ready');
|
||||
expect(registered).toContain('init');
|
||||
expect(registered).toContain('setData');
|
||||
expect(registered).toContain('setSeries');
|
||||
expect(registered).toContain('setLegend');
|
||||
expect(registered).toContain('setCursor');
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Tooltip rendering ------------------------------------------------------
|
||||
|
||||
describe('tooltip rendering', () => {
|
||||
it('renders contents into a portal on document.body when hover is active', () => {
|
||||
const config = createConfigMock();
|
||||
const renderTooltip = jest.fn(() =>
|
||||
React.createElement('div', null, 'tooltip-body'),
|
||||
);
|
||||
|
||||
renderAndActivateHover(config, renderTooltip);
|
||||
|
||||
expect(renderTooltip).toHaveBeenCalled();
|
||||
expect(screen.getByText('tooltip-body')).toBeInTheDocument();
|
||||
|
||||
const container = document.querySelector(
|
||||
'.tooltip-plugin-container',
|
||||
) as HTMLElement;
|
||||
expect(container).not.toBeNull();
|
||||
expect(container.parentElement).toBe(document.body);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Pin behaviour ----------------------------------------------------------
|
||||
|
||||
describe('pin behaviour', () => {
|
||||
it('pins the tooltip when canPinTooltip is true and overlay is clicked', () => {
|
||||
const config = createConfigMock();
|
||||
|
||||
const fakePlot = renderAndActivateHover(config, undefined, {
|
||||
canPinTooltip: true,
|
||||
});
|
||||
|
||||
const container = document.querySelector(
|
||||
'.tooltip-plugin-container',
|
||||
) as HTMLElement;
|
||||
expect(container.classList.contains('pinned')).toBe(false);
|
||||
|
||||
act(() => {
|
||||
fakePlot.over.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
return waitFor(() => {
|
||||
const updated = document.querySelector(
|
||||
'.tooltip-plugin-container',
|
||||
) as HTMLElement | null;
|
||||
expect(updated).not.toBeNull();
|
||||
expect(updated?.classList.contains('pinned')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('dismisses a pinned tooltip via the dismiss callback', async () => {
|
||||
const config = createConfigMock();
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config,
|
||||
render: (args: TooltipRenderArgs) =>
|
||||
React.createElement(
|
||||
'button',
|
||||
{ type: 'button', onClick: args.dismiss },
|
||||
'Dismiss',
|
||||
),
|
||||
syncMode: DashboardCursorSync.None,
|
||||
canPinTooltip: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const fakePlot = createFakePlot();
|
||||
|
||||
act(() => {
|
||||
getHandler(config, 'init')(fakePlot);
|
||||
getHandler(config, 'setSeries')(fakePlot, 1, { focus: true });
|
||||
});
|
||||
|
||||
// Pin the tooltip.
|
||||
act(() => {
|
||||
fakePlot.over.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
// Wait until the tooltip is actually pinned (pointer events enabled)
|
||||
await waitFor(() => {
|
||||
const container = document.querySelector(
|
||||
'.tooltip-plugin-container',
|
||||
) as HTMLElement | null;
|
||||
expect(container).not.toBeNull();
|
||||
expect(container?.classList.contains('pinned')).toBe(true);
|
||||
});
|
||||
|
||||
const button = await screen.findByRole('button', { name: 'Dismiss' });
|
||||
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
await user.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('drops a pinned tooltip when the underlying data changes', () => {
|
||||
jest.useFakeTimers();
|
||||
const config = createConfigMock();
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config: config,
|
||||
render: () => React.createElement('div', null, 'tooltip-body'),
|
||||
syncMode: DashboardCursorSync.None,
|
||||
canPinTooltip: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const fakePlot = createFakePlot();
|
||||
|
||||
act(() => {
|
||||
getHandler(config, 'init')(fakePlot);
|
||||
getHandler(config, 'setSeries')(fakePlot, 1, { focus: true });
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
// Pin.
|
||||
act(() => {
|
||||
fakePlot.over.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(
|
||||
(document.querySelector(
|
||||
'.tooltip-plugin-container',
|
||||
) as HTMLElement)?.classList.contains('pinned'),
|
||||
).toBe(true);
|
||||
|
||||
// Simulate data update – should dismiss the pinned tooltip.
|
||||
act(() => {
|
||||
getHandler(config, 'setData')(fakePlot);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('unpins the tooltip on outside mousedown', () => {
|
||||
jest.useFakeTimers();
|
||||
const config = createConfigMock();
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config,
|
||||
render: () => React.createElement('div', null, 'pinned content'),
|
||||
syncMode: DashboardCursorSync.None,
|
||||
canPinTooltip: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const fakePlot = createFakePlot();
|
||||
|
||||
act(() => {
|
||||
getHandler(config, 'init')(fakePlot);
|
||||
getHandler(config, 'setSeries')(fakePlot, 1, { focus: true });
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fakePlot.over.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(
|
||||
document
|
||||
.querySelector('.tooltip-plugin-container')
|
||||
?.classList.contains('pinned'),
|
||||
).toBe(true);
|
||||
|
||||
// Click outside the tooltip container.
|
||||
act(() => {
|
||||
document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('unpins the tooltip on outside keydown', () => {
|
||||
jest.useFakeTimers();
|
||||
const config = createConfigMock();
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config,
|
||||
render: () => React.createElement('div', null, 'pinned content'),
|
||||
syncMode: DashboardCursorSync.None,
|
||||
canPinTooltip: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const fakePlot = createFakePlot();
|
||||
|
||||
act(() => {
|
||||
getHandler(config, 'init')(fakePlot);
|
||||
getHandler(config, 'setSeries')(fakePlot, 1, { focus: true });
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fakePlot.over.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(
|
||||
document
|
||||
.querySelector('.tooltip-plugin-container')
|
||||
?.classList.contains('pinned'),
|
||||
).toBe(true);
|
||||
|
||||
// Press a key outside the tooltip.
|
||||
act(() => {
|
||||
document.body.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }),
|
||||
);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Cursor sync ------------------------------------------------------------
|
||||
|
||||
describe('cursor sync', () => {
|
||||
it('enables uPlot cursor sync for time-based scales when mode is Tooltip', () => {
|
||||
const config = createConfigMock();
|
||||
const setCursorSpy = jest.spyOn(config, 'setCursor');
|
||||
config.addScale({ scaleKey: 'x', time: true });
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config,
|
||||
render: () => null,
|
||||
syncMode: DashboardCursorSync.Tooltip,
|
||||
syncKey: 'dashboard-sync',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(setCursorSpy).toHaveBeenCalledWith({
|
||||
sync: { key: 'dashboard-sync', scales: ['x', null] },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not enable cursor sync when mode is None', () => {
|
||||
const config = createConfigMock();
|
||||
const setCursorSpy = jest.spyOn(config, 'setCursor');
|
||||
config.addScale({ scaleKey: 'x', time: true });
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config,
|
||||
render: () => null,
|
||||
syncMode: DashboardCursorSync.None,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(setCursorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not enable cursor sync when scale is not time-based', () => {
|
||||
const config = createConfigMock();
|
||||
const setCursorSpy = jest.spyOn(config, 'setCursor');
|
||||
config.addScale({ scaleKey: 'x', time: false });
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config,
|
||||
render: () => null,
|
||||
syncMode: DashboardCursorSync.Tooltip,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(setCursorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Cleanup ----------------------------------------------------------------
|
||||
|
||||
describe('cleanup on unmount', () => {
|
||||
it('removes window event listeners and all uPlot hooks', () => {
|
||||
const config = createConfigMock();
|
||||
const addSpy = jest.spyOn(window, 'addEventListener');
|
||||
const removeSpy = jest.spyOn(window, 'removeEventListener');
|
||||
|
||||
const { unmount } = render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config,
|
||||
render: () => null,
|
||||
syncMode: DashboardCursorSync.None,
|
||||
}),
|
||||
);
|
||||
|
||||
const resizeCall = addSpy.mock.calls.find(([type]) => type === 'resize');
|
||||
const scrollCall = addSpy.mock.calls.find(([type]) => type === 'scroll');
|
||||
|
||||
expect(resizeCall).toBeDefined();
|
||||
expect(scrollCall).toBeDefined();
|
||||
|
||||
const resizeListener = resizeCall?.[1] as EventListener;
|
||||
const scrollListener = scrollCall?.[1] as EventListener;
|
||||
const scrollOptions = scrollCall?.[2];
|
||||
|
||||
unmount();
|
||||
|
||||
config.removeCallbacks.forEach((removeFn) => {
|
||||
expect(removeFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(removeSpy).toHaveBeenCalledWith('resize', resizeListener);
|
||||
expect(removeSpy).toHaveBeenCalledWith(
|
||||
'scroll',
|
||||
scrollListener,
|
||||
scrollOptions,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
80
frontend/src/lib/uPlotV2/utils/axis.ts
Normal file
80
frontend/src/lib/uPlotV2/utils/axis.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Axis } from 'uplot';
|
||||
|
||||
/**
|
||||
* Calculate text width for longest value
|
||||
*/
|
||||
export function calculateTextWidth(
|
||||
self: uPlot,
|
||||
axis: Axis,
|
||||
values: string[] | undefined,
|
||||
): number {
|
||||
if (!values || values.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Find longest value
|
||||
const longestVal = values.reduce(
|
||||
(acc, val) => (val.length > acc.length ? val : acc),
|
||||
'',
|
||||
);
|
||||
|
||||
if (longestVal === '' || !axis.font?.[0]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
self.ctx.font = axis.font[0];
|
||||
return self.ctx.measureText(longestVal).width / devicePixelRatio;
|
||||
}
|
||||
|
||||
export function getExistingAxisSize({
|
||||
uplotInstance,
|
||||
axis,
|
||||
values,
|
||||
axisIdx,
|
||||
cycleNum,
|
||||
}: {
|
||||
uplotInstance: uPlot;
|
||||
axis: Axis;
|
||||
values?: string[];
|
||||
axisIdx: number;
|
||||
cycleNum: number;
|
||||
}): number {
|
||||
const internalSize = (axis as { _size?: number })._size;
|
||||
if (internalSize !== undefined) {
|
||||
return internalSize;
|
||||
}
|
||||
|
||||
const existingSize = axis.size;
|
||||
if (typeof existingSize === 'function') {
|
||||
return existingSize(uplotInstance, values ?? [], axisIdx, cycleNum);
|
||||
}
|
||||
|
||||
return existingSize ?? 0;
|
||||
}
|
||||
|
||||
export function buildYAxisSizeCalculator(gap: number): uPlot.Axis.Size {
|
||||
return (
|
||||
self: uPlot,
|
||||
values: string[] | undefined,
|
||||
axisIdx: number,
|
||||
cycleNum: number,
|
||||
): number => {
|
||||
const axis = self.axes[axisIdx];
|
||||
|
||||
// Bail out, force convergence
|
||||
if (cycleNum > 1) {
|
||||
return getExistingAxisSize({
|
||||
uplotInstance: self,
|
||||
axis,
|
||||
values,
|
||||
axisIdx,
|
||||
cycleNum,
|
||||
});
|
||||
}
|
||||
|
||||
let axisSize = (axis.ticks?.size ?? 0) + gap;
|
||||
axisSize += calculateTextWidth(self, axis, values);
|
||||
|
||||
return Math.ceil(axisSize);
|
||||
};
|
||||
}
|
||||
@@ -7,6 +7,4 @@ import { handlers } from './handlers';
|
||||
// This configures a request mocking server with the given request handlers.
|
||||
export const server = setupServer(...handlers);
|
||||
|
||||
export * from './utils';
|
||||
|
||||
export { rest };
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { ResponseResolver, restContext, RestRequest } from 'msw';
|
||||
|
||||
export const createErrorResponse = (
|
||||
status: number,
|
||||
code: string,
|
||||
message: string,
|
||||
): ResponseResolver<RestRequest, typeof restContext> => (
|
||||
_req,
|
||||
res,
|
||||
ctx,
|
||||
): ReturnType<ResponseResolver<RestRequest, typeof restContext>> =>
|
||||
res(
|
||||
ctx.status(status),
|
||||
ctx.json({
|
||||
error: {
|
||||
code,
|
||||
message,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const handleInternalServerError = createErrorResponse(
|
||||
500,
|
||||
'INTERNAL_SERVER_ERROR',
|
||||
'Internal server error occurred',
|
||||
);
|
||||
@@ -1,46 +0,0 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { render, waitFor } from 'tests/test-utils';
|
||||
|
||||
import ForgotPassword from '../index';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
push: jest.fn(),
|
||||
location: {
|
||||
search: '',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const mockHistoryPush = history.push as jest.MockedFunction<
|
||||
typeof history.push
|
||||
>;
|
||||
|
||||
describe('ForgotPassword Page', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Route State Handling', () => {
|
||||
it('redirects to login when route state is missing', async () => {
|
||||
render(<ForgotPassword />, undefined, {
|
||||
initialRoute: '/forgot-password',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith(ROUTES.LOGIN);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null when route state is missing', () => {
|
||||
const { container } = render(<ForgotPassword />, undefined, {
|
||||
initialRoute: '/forgot-password',
|
||||
});
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import AuthPageContainer from 'components/AuthPageContainer';
|
||||
import ROUTES from 'constants/routes';
|
||||
import ForgotPasswordContainer, {
|
||||
ForgotPasswordRouteState,
|
||||
} from 'container/ForgotPassword';
|
||||
import history from 'lib/history';
|
||||
|
||||
import '../Login/Login.styles.scss';
|
||||
|
||||
function ForgotPassword(): JSX.Element | null {
|
||||
const location = useLocation<ForgotPasswordRouteState | undefined>();
|
||||
const routeState = location.state;
|
||||
|
||||
useEffect(() => {
|
||||
if (!routeState?.email) {
|
||||
history.push(ROUTES.LOGIN);
|
||||
}
|
||||
}, [routeState]);
|
||||
|
||||
if (!routeState?.email) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthPageContainer>
|
||||
<div className="auth-form-card">
|
||||
<ForgotPasswordContainer
|
||||
email={routeState.email}
|
||||
orgId={routeState.orgId}
|
||||
orgs={routeState.orgs}
|
||||
/>
|
||||
</div>
|
||||
</AuthPageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default ForgotPassword;
|
||||
@@ -412,16 +412,14 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
||||
});
|
||||
|
||||
// Verify dashboard state contains the variables with default values
|
||||
await waitFor(() => {
|
||||
const dashboardVariables = screen.getByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
const dashboardVariables = await screen.findByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
|
||||
expect(parsedVariables).toHaveProperty('environment');
|
||||
expect(parsedVariables).toHaveProperty('services');
|
||||
// Default allSelected values should be preserved
|
||||
expect(parsedVariables.environment.allSelected).toBe(false);
|
||||
expect(parsedVariables.services.allSelected).toBe(false);
|
||||
});
|
||||
expect(parsedVariables).toHaveProperty('environment');
|
||||
expect(parsedVariables).toHaveProperty('services');
|
||||
// Default allSelected values should be preserved
|
||||
expect(parsedVariables.environment.allSelected).toBe(false);
|
||||
expect(parsedVariables.services.allSelected).toBe(false);
|
||||
});
|
||||
|
||||
it('should merge URL variables with dashboard data and normalize values correctly', async () => {
|
||||
@@ -468,26 +466,16 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
||||
});
|
||||
|
||||
// Verify the dashboard state reflects the normalized URL values
|
||||
await waitFor(() => {
|
||||
const dashboardVariables = screen.getByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
const dashboardVariables = await screen.findByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
|
||||
// First ensure the variables exist
|
||||
expect(parsedVariables).toHaveProperty('environment');
|
||||
expect(parsedVariables).toHaveProperty('services');
|
||||
// The selectedValue should be updated with normalized URL values
|
||||
expect(parsedVariables.environment.selectedValue).toBe('development');
|
||||
expect(parsedVariables.services.selectedValue).toEqual(['db', 'cache']);
|
||||
|
||||
// Then check their properties
|
||||
expect(parsedVariables.environment).toHaveProperty('selectedValue');
|
||||
expect(parsedVariables.services).toHaveProperty('selectedValue');
|
||||
|
||||
// The selectedValue should be updated with normalized URL values
|
||||
expect(parsedVariables.environment.selectedValue).toBe('development');
|
||||
expect(parsedVariables.services.selectedValue).toEqual(['db', 'cache']);
|
||||
|
||||
// allSelected should be set to false when URL values override
|
||||
expect(parsedVariables.environment.allSelected).toBe(false);
|
||||
expect(parsedVariables.services.allSelected).toBe(false);
|
||||
});
|
||||
// allSelected should be set to false when URL values override
|
||||
expect(parsedVariables.environment.allSelected).toBe(false);
|
||||
expect(parsedVariables.services.allSelected).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle ALL_SELECTED_VALUE from URL and set allSelected correctly', async () => {
|
||||
@@ -512,8 +500,8 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
||||
);
|
||||
|
||||
// Verify that allSelected is set to true for the services variable
|
||||
await waitFor(() => {
|
||||
const dashboardVariables = screen.getByTestId('dashboard-variables');
|
||||
await waitFor(async () => {
|
||||
const dashboardVariables = await screen.findByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
|
||||
expect(parsedVariables.services.allSelected).toBe(true);
|
||||
@@ -615,8 +603,8 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
});
|
||||
|
||||
// Verify that defaultValue is set from textboxValue
|
||||
await waitFor(() => {
|
||||
const dashboardVariables = screen.getByTestId('dashboard-variables');
|
||||
await waitFor(async () => {
|
||||
const dashboardVariables = await screen.findByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
|
||||
expect(parsedVariables.myTextbox.type).toBe('TEXTBOX');
|
||||
@@ -660,8 +648,8 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
});
|
||||
|
||||
// Verify that existing defaultValue is preserved
|
||||
await waitFor(() => {
|
||||
const dashboardVariables = screen.getByTestId('dashboard-variables');
|
||||
await waitFor(async () => {
|
||||
const dashboardVariables = await screen.findByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
|
||||
expect(parsedVariables.myTextbox.type).toBe('TEXTBOX');
|
||||
@@ -706,8 +694,8 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
});
|
||||
|
||||
// Verify that defaultValue is set to empty string
|
||||
await waitFor(() => {
|
||||
const dashboardVariables = screen.getByTestId('dashboard-variables');
|
||||
await waitFor(async () => {
|
||||
const dashboardVariables = await screen.findByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
|
||||
expect(parsedVariables.myTextbox.type).toBe('TEXTBOX');
|
||||
@@ -751,8 +739,8 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
});
|
||||
|
||||
// Verify that defaultValue is NOT set from textboxValue for QUERY type
|
||||
await waitFor(() => {
|
||||
const dashboardVariables = screen.getByTestId('dashboard-variables');
|
||||
await waitFor(async () => {
|
||||
const dashboardVariables = await screen.findByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
|
||||
expect(parsedVariables.myQuery.type).toBe('QUERY');
|
||||
|
||||
@@ -68,7 +68,6 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
ALERT_HISTORY: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
ALERT_OVERVIEW: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
LOGIN: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
FORGOT_PASSWORD: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
NOT_FOUND: ['ADMIN', 'VIEWER', 'EDITOR'],
|
||||
PASSWORD_RESET: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SERVICE_METRICS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
|
||||
@@ -5038,7 +5038,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/design-tokens/-/design-tokens-2.1.1.tgz#9c36d433fd264410713cc0c5ebdd75ce0ebecba3"
|
||||
integrity sha512-SdziCHg5Lwj+6oY6IRUPplaKZ+kTHjbrlhNj//UoAJ8aQLnRdR2F/miPzfSi4vrYw88LtXxNA9J9iJyacCp37A==
|
||||
|
||||
"@signozhq/icons@0.1.0", "@signozhq/icons@^0.1.0":
|
||||
"@signozhq/icons@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/icons/-/icons-0.1.0.tgz#00dfb430dbac423bfff715876f91a7b8a72509e4"
|
||||
integrity sha512-kGWDhCpQkFWaNwyWfy88AIbg902wBbgTFTBAtmo6DkHyLGoqWAf0Jcq8BX+7brFqJF9PnLoSJDj1lvCpUsI/Ig==
|
||||
|
||||
@@ -208,16 +208,7 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
event.GroupByApplied = len(spec.GroupBy) > 0
|
||||
|
||||
if spec.Source == telemetrytypes.SourceMeter {
|
||||
if spec.StepInterval.Seconds() == 0 {
|
||||
spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.RecommendedStepIntervalForMeter(req.Start, req.End))}
|
||||
}
|
||||
|
||||
if spec.StepInterval.Seconds() < float64(querybuilder.MinAllowedStepIntervalForMeter(req.Start, req.End)) {
|
||||
newStep := qbtypes.Step{
|
||||
Duration: time.Second * time.Duration(querybuilder.MinAllowedStepIntervalForMeter(req.Start, req.End)),
|
||||
}
|
||||
spec.StepInterval = newStep
|
||||
}
|
||||
spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.RecommendedStepIntervalForMeter(req.Start, req.End))}
|
||||
} else {
|
||||
if spec.StepInterval.Seconds() == 0 {
|
||||
spec.StepInterval = qbtypes.Step{
|
||||
|
||||
@@ -830,7 +830,7 @@ func (r *ClickHouseReader) GetUsage(ctx context.Context, queryParams *model.GetU
|
||||
|
||||
func (r *ClickHouseReader) GetSpansForTrace(ctx context.Context, traceID string, traceDetailsQuery string) ([]model.SpanItemV2, *model.ApiError) {
|
||||
var traceSummary model.TraceSummary
|
||||
summaryQuery := fmt.Sprintf("SELECT trace_id, min(start) AS start, max(end) AS end, sum(num_spans) AS num_spans FROM %s.%s WHERE trace_id=$1 GROUP BY trace_id", r.TraceDB, r.traceSummaryTable)
|
||||
summaryQuery := fmt.Sprintf("SELECT * from %s.%s FINAL WHERE trace_id=$1", r.TraceDB, r.traceSummaryTable)
|
||||
err := r.db.QueryRow(ctx, summaryQuery, traceID).Scan(&traceSummary.TraceID, &traceSummary.Start, &traceSummary.End, &traceSummary.NumSpans)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -6458,7 +6458,7 @@ func (r *ClickHouseReader) SearchTraces(ctx context.Context, params *model.Searc
|
||||
}
|
||||
|
||||
var traceSummary model.TraceSummary
|
||||
summaryQuery := fmt.Sprintf("SELECT trace_id, min(start) AS start, max(end) AS end, sum(num_spans) AS num_spans FROM %s.%s WHERE trace_id=$1 GROUP BY trace_id", r.TraceDB, r.traceSummaryTable)
|
||||
summaryQuery := fmt.Sprintf("SELECT * from %s.%s FINAL WHERE trace_id=$1", r.TraceDB, r.traceSummaryTable)
|
||||
err := r.db.QueryRow(ctx, summaryQuery, params.TraceID).Scan(&traceSummary.TraceID, &traceSummary.Start, &traceSummary.End, &traceSummary.NumSpans)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
|
||||
@@ -89,23 +89,6 @@ func RecommendedStepIntervalForMeter(start, end uint64) uint64 {
|
||||
return recommended
|
||||
}
|
||||
|
||||
func MinAllowedStepIntervalForMeter(start, end uint64) uint64 {
|
||||
start = ToNanoSecs(start)
|
||||
end = ToNanoSecs(end)
|
||||
|
||||
step := (end - start) / RecommendedNumberOfPoints / 1e9
|
||||
|
||||
// for meter queries the minimum step interval allowed is 1 hour as this is our granularity
|
||||
if step < 3600 {
|
||||
return 3600
|
||||
}
|
||||
|
||||
// return the nearest lower multiple of 3600 ( 1 hour )
|
||||
minAllowed := step - step%3600
|
||||
|
||||
return minAllowed
|
||||
}
|
||||
|
||||
func RecommendedStepIntervalForMetric(start, end uint64) uint64 {
|
||||
start = ToNanoSecs(start)
|
||||
end = ToNanoSecs(end)
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestScalarData_MarshalJSON(t *testing.T) {
|
||||
{4.0, 5.0, 6.0},
|
||||
},
|
||||
},
|
||||
expected: `{"queryName":"test_query","columns":[{"name":"value","queryName":"test_query","aggregationIndex":0,"meta":{},"columnType":"aggregation"}],"data":[[1,2,3],[4,5,6]]}`,
|
||||
expected: `{"queryName":"test_query","columns":[{"name":"value","signal":"","fieldContext":"","fieldDataType":"","queryName":"test_query","aggregationIndex":0,"meta":{},"columnType":"aggregation"}],"data":[[1,2,3],[4,5,6]]}`,
|
||||
},
|
||||
{
|
||||
name: "scalar data with NaN",
|
||||
@@ -149,7 +149,7 @@ func TestScalarData_MarshalJSON(t *testing.T) {
|
||||
{math.Inf(1), 5.0, math.Inf(-1)},
|
||||
},
|
||||
},
|
||||
expected: `{"queryName":"test_query","columns":[{"name":"value","queryName":"test_query","aggregationIndex":0,"meta":{},"columnType":"aggregation"}],"data":[[1,"NaN",3],["Inf",5,"-Inf"]]}`,
|
||||
expected: `{"queryName":"test_query","columns":[{"name":"value","signal":"","fieldContext":"","fieldDataType":"","queryName":"test_query","aggregationIndex":0,"meta":{},"columnType":"aggregation"}],"data":[[1,"NaN",3],["Inf",5,"-Inf"]]}`,
|
||||
},
|
||||
{
|
||||
name: "scalar data with mixed types",
|
||||
@@ -168,7 +168,7 @@ func TestScalarData_MarshalJSON(t *testing.T) {
|
||||
{nil, math.Inf(1), 3.14, false},
|
||||
},
|
||||
},
|
||||
expected: `{"queryName":"test_query","columns":[{"name":"mixed","queryName":"test_query","aggregationIndex":0,"meta":{},"columnType":"aggregation"}],"data":[["string",42,"NaN",true],[null,"Inf",3.14,false]]}`,
|
||||
expected: `{"queryName":"test_query","columns":[{"name":"mixed","signal":"","fieldContext":"","fieldDataType":"","queryName":"test_query","aggregationIndex":0,"meta":{},"columnType":"aggregation"}],"data":[["string",42,"NaN",true],[null,"Inf",3.14,false]]}`,
|
||||
},
|
||||
{
|
||||
name: "scalar data with nested structures",
|
||||
@@ -189,7 +189,7 @@ func TestScalarData_MarshalJSON(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `{"queryName":"test_query","columns":[{"name":"nested","queryName":"test_query","aggregationIndex":0,"meta":{},"columnType":"aggregation"}],"data":[[{"count":10,"value":"NaN"},[1,"Inf",3]]]}`,
|
||||
expected: `{"queryName":"test_query","columns":[{"name":"nested","signal":"","fieldContext":"","fieldDataType":"","queryName":"test_query","aggregationIndex":0,"meta":{},"columnType":"aggregation"}],"data":[[{"count":10,"value":"NaN"},[1,"Inf",3]]]}`,
|
||||
},
|
||||
{
|
||||
name: "empty scalar data",
|
||||
|
||||
@@ -28,12 +28,12 @@ const (
|
||||
)
|
||||
|
||||
type TelemetryFieldKey struct {
|
||||
Name string `json:"name" required:"true"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Signal Signal `json:"signal,omitzero"`
|
||||
FieldContext FieldContext `json:"fieldContext,omitzero"`
|
||||
FieldDataType FieldDataType `json:"fieldDataType,omitzero"`
|
||||
Signal Signal `json:"signal,omitempty"`
|
||||
FieldContext FieldContext `json:"fieldContext,omitempty"`
|
||||
FieldDataType FieldDataType `json:"fieldDataType,omitempty"`
|
||||
|
||||
JSONDataType *JSONDataType `json:"-"`
|
||||
JSONPlan JSONAccessPlan `json:"-"`
|
||||
@@ -268,8 +268,8 @@ type FieldValueSelector struct {
|
||||
}
|
||||
|
||||
type GettableFieldKeys struct {
|
||||
Keys map[string][]*TelemetryFieldKey `json:"keys" required:"true"`
|
||||
Complete bool `json:"complete" required:"true"`
|
||||
Keys map[string][]*TelemetryFieldKey `json:"keys"`
|
||||
Complete bool `json:"complete"`
|
||||
}
|
||||
|
||||
type PostableFieldKeysParams struct {
|
||||
@@ -285,8 +285,8 @@ type PostableFieldKeysParams struct {
|
||||
}
|
||||
|
||||
type GettableFieldValues struct {
|
||||
Values *TelemetryFieldValues `json:"values" required:"true"`
|
||||
Complete bool `json:"complete" required:"true"`
|
||||
Values *TelemetryFieldValues `json:"values"`
|
||||
Complete bool `json:"complete"`
|
||||
}
|
||||
|
||||
type PostableFieldValueParams struct {
|
||||
|
||||
@@ -1167,6 +1167,9 @@ def test_logs_time_series_count(
|
||||
{
|
||||
"key": {
|
||||
"name": "host.name",
|
||||
"signal": "",
|
||||
"fieldContext": "",
|
||||
"fieldDataType": "",
|
||||
},
|
||||
"value": "linux-001",
|
||||
}
|
||||
@@ -1197,6 +1200,9 @@ def test_logs_time_series_count(
|
||||
{
|
||||
"key": {
|
||||
"name": "host.name",
|
||||
"signal": "",
|
||||
"fieldContext": "",
|
||||
"fieldDataType": "",
|
||||
},
|
||||
"value": "linux-000",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user