Compare commits

..

5 Commits

Author SHA1 Message Date
Piyush Singariya
bac0e5f499 Merge branch 'main' into qb-json-fixes 2026-02-03 11:55:42 +05:30
Piyush Singariya
391e889f96 fix: comment 2026-02-02 16:26:13 +05:30
Piyush Singariya
9254b879a9 Merge branch 'main' into qb-json-fixes 2026-02-02 16:10:25 +05:30
Piyush Singariya
ac62103228 Merge branch 'main' into qb-json-fixes 2026-02-02 13:58:15 +05:30
Piyush Singariya
491bf14bd0 fix: json qb array expression 2026-02-02 13:56:04 +05:30
177 changed files with 1236 additions and 13386 deletions

View File

@@ -42,11 +42,10 @@ jobs:
- callbackauthn
- cloudintegrations
- dashboard
- logspipelines
- preference
- querier
- role
- ttl
- preference
- logspipelines
- alerts
sqlstore-provider:
- postgres

View File

@@ -11,8 +11,5 @@
"[go]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go"
},
"[sql]": {
"editor.defaultFormatter": "adpyke.vscode-sql-formatter"
}
}

View File

@@ -2285,15 +2285,6 @@ paths:
deprecated: false
description: This endpoint returns the ingestion keys for a workspace
operationId: GetIngestionKeys
parameters:
- in: query
name: page
schema:
type: integer
- in: query
name: per_page
schema:
type: integer
responses:
"200":
content:
@@ -2606,19 +2597,6 @@ paths:
deprecated: false
description: This endpoint returns the ingestion keys for a workspace
operationId: SearchIngestionKeys
parameters:
- in: query
name: name
schema:
type: string
- in: query
name: page
schema:
type: integer
- in: query
name: per_page
schema:
type: integer
responses:
"200":
content:

View File

@@ -79,7 +79,7 @@ func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publi
authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()),
)
err = module.roleSetter.PatchObjects(ctx, orgID, role.Name, authtypes.RelationRead, []*authtypes.Object{additionObject}, nil)
err = module.roleSetter.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, []*authtypes.Object{additionObject}, nil)
if err != nil {
return err
}
@@ -208,7 +208,7 @@ func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashb
authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()),
)
err = module.roleSetter.PatchObjects(ctx, orgID, role.Name, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
err = module.roleSetter.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
if err != nil {
return err
}
@@ -285,7 +285,7 @@ func (module *module) deletePublic(ctx context.Context, orgID valuer.UUID, dashb
authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()),
)
err = module.roleSetter.PatchObjects(ctx, orgID, role.Name, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
err = module.roleSetter.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
if err != nil {
return err
}

View File

@@ -116,18 +116,18 @@ func (setter *setter) Patch(ctx context.Context, orgID valuer.UUID, role *rolety
return setter.store.Update(ctx, orgID, roletypes.NewStorableRoleFromRole(role))
}
func (setter *setter) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
func (setter *setter) PatchObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
_, err := setter.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
additionTuples, err := roletypes.GetAdditionTuples(name, orgID, relation, additions)
additionTuples, err := roletypes.GetAdditionTuples(id, orgID, relation, additions)
if err != nil {
return err
}
deletionTuples, err := roletypes.GetDeletionTuples(name, orgID, relation, deletions)
deletionTuples, err := roletypes.GetDeletionTuples(id, orgID, relation, deletions)
if err != nil {
return err
}

View File

@@ -26,7 +26,6 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err)
}
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
// create a threshold rule
tr, err := baserules.NewThresholdRule(

View File

@@ -51,7 +51,7 @@
"@signozhq/checkbox": "0.0.2",
"@signozhq/combobox": "0.0.2",
"@signozhq/command": "0.0.0",
"@signozhq/design-tokens": "2.1.1",
"@signozhq/design-tokens": "1.1.4",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
"@signozhq/resizable": "0.0.0",

View File

@@ -39,7 +39,7 @@ type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint deletes the public sharing config and disables the public sharing of a dashboard
* This endpoints deletes the public sharing config and disables the public sharing of a dashboard
* @summary Delete public dashboard
*/
export const deletePublicDashboard = ({
@@ -118,7 +118,7 @@ export const useDeletePublicDashboard = <
return useMutation(mutationOptions);
};
/**
* This endpoint returns public sharing config for a dashboard
* This endpoints returns public sharing config for a dashboard
* @summary Get public dashboard
*/
export const getPublicDashboard = (
@@ -222,7 +222,7 @@ export const invalidateGetPublicDashboard = async (
};
/**
* This endpoint creates public sharing config and enables public sharing of the dashboard
* This endpoints creates public sharing config and enables public sharing of the dashboard
* @summary Create public dashboard
*/
export const createPublicDashboard = (
@@ -321,7 +321,7 @@ export const useCreatePublicDashboard = <
return useMutation(mutationOptions);
};
/**
* This endpoint updates the public sharing config for a dashboard
* This endpoints updates the public sharing config for a dashboard
* @summary Update public dashboard
*/
export const updatePublicDashboard = (
@@ -418,7 +418,7 @@ export const useUpdatePublicDashboard = <
return useMutation(mutationOptions);
};
/**
* This endpoint returns the sanitized dashboard data for public access
* This endpoints returns the sanitized dashboard data for public access
* @summary Get public dashboard data
*/
export const getPublicDashboardData = (

View File

@@ -25,7 +25,7 @@ type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns global config
* This endpoints returns global config
* @summary Get global config
*/
export const getGlobalConfig = (signal?: AbortSignal) => {

View File

@@ -1,433 +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,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useMutation, useQuery } from 'react-query';
import { GeneratedAPIInstance } from '../../../index';
import type {
CreateRole201,
DeleteRolePathParameters,
GetRole200,
GetRolePathParameters,
ListRoles200,
PatchRolePathParameters,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint lists all roles
* @summary List roles
*/
export const listRoles = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListRoles200>({
url: `/api/v1/roles`,
method: 'GET',
signal,
});
};
export const getListRolesQueryKey = () => {
return ['listRoles'] as const;
};
export const getListRolesQueryOptions = <
TData = Awaited<ReturnType<typeof listRoles>>,
TError = RenderErrorResponseDTO
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof listRoles>>, TError, TData>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListRolesQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listRoles>>> = ({
signal,
}) => listRoles(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listRoles>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListRolesQueryResult = NonNullable<
Awaited<ReturnType<typeof listRoles>>
>;
export type ListRolesQueryError = RenderErrorResponseDTO;
/**
* @summary List roles
*/
export function useListRoles<
TData = Awaited<ReturnType<typeof listRoles>>,
TError = RenderErrorResponseDTO
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof listRoles>>, TError, TData>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListRolesQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List roles
*/
export const invalidateListRoles = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListRolesQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint creates a role
* @summary Create role
*/
export const createRole = (signal?: AbortSignal) => {
return GeneratedAPIInstance<CreateRole201>({
url: `/api/v1/roles`,
method: 'POST',
signal,
});
};
export const getCreateRoleMutationOptions = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
void,
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
void,
TContext
> => {
const mutationKey = ['createRole'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createRole>>,
void
> = () => {
return createRole();
};
return { mutationFn, ...mutationOptions };
};
export type CreateRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createRole>>
>;
export type CreateRoleMutationError = RenderErrorResponseDTO;
/**
* @summary Create role
*/
export const useCreateRole = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
void,
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createRole>>,
TError,
void,
TContext
> => {
const mutationOptions = getCreateRoleMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint deletes a role
* @summary Delete role
*/
export const deleteRole = ({ id }: DeleteRolePathParameters) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/roles/${id}`,
method: 'DELETE',
});
};
export const getDeleteRoleMutationOptions = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteRole>>,
TError,
{ pathParams: DeleteRolePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteRole>>,
TError,
{ pathParams: DeleteRolePathParameters },
TContext
> => {
const mutationKey = ['deleteRole'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteRole>>,
{ pathParams: DeleteRolePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteRole(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteRole>>
>;
export type DeleteRoleMutationError = RenderErrorResponseDTO;
/**
* @summary Delete role
*/
export const useDeleteRole = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteRole>>,
TError,
{ pathParams: DeleteRolePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteRole>>,
TError,
{ pathParams: DeleteRolePathParameters },
TContext
> => {
const mutationOptions = getDeleteRoleMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint gets a role
* @summary Get role
*/
export const getRole = (
{ id }: GetRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRole200>({
url: `/api/v1/roles/${id}`,
method: 'GET',
signal,
});
};
export const getGetRoleQueryKey = ({ id }: GetRolePathParameters) => {
return ['getRole'] as const;
};
export const getGetRoleQueryOptions = <
TData = Awaited<ReturnType<typeof getRole>>,
TError = RenderErrorResponseDTO
>(
{ id }: GetRolePathParameters,
options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getRole>>, TError, TData>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetRoleQueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getRole>>> = ({
signal,
}) => getRole({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof getRole>>, TError, TData> & {
queryKey: QueryKey;
};
};
export type GetRoleQueryResult = NonNullable<
Awaited<ReturnType<typeof getRole>>
>;
export type GetRoleQueryError = RenderErrorResponseDTO;
/**
* @summary Get role
*/
export function useGetRole<
TData = Awaited<ReturnType<typeof getRole>>,
TError = RenderErrorResponseDTO
>(
{ id }: GetRolePathParameters,
options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getRole>>, TError, TData>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRoleQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get role
*/
export const invalidateGetRole = async (
queryClient: QueryClient,
{ id }: GetRolePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRoleQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint patches a role
* @summary Patch role
*/
export const patchRole = ({ id }: PatchRolePathParameters) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/roles/${id}`,
method: 'PATCH',
});
};
export const getPatchRoleMutationOptions = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof patchRole>>,
TError,
{ pathParams: PatchRolePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof patchRole>>,
TError,
{ pathParams: PatchRolePathParameters },
TContext
> => {
const mutationKey = ['patchRole'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof patchRole>>,
{ pathParams: PatchRolePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return patchRole(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type PatchRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof patchRole>>
>;
export type PatchRoleMutationError = RenderErrorResponseDTO;
/**
* @summary Patch role
*/
export const usePatchRole = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof patchRole>>,
TError,
{ pathParams: PatchRolePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof patchRole>>,
TError,
{ pathParams: PatchRolePathParameters },
TContext
> => {
const mutationOptions = getPatchRoleMutationOptions(options);
return useMutation(mutationOptions);
};

View File

@@ -1110,39 +1110,6 @@ export interface RenderErrorResponseDTO {
status?: string;
}
export interface RoletypesRoleDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id?: string;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
orgId?: string;
/**
* @type string
*/
type?: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
}
export interface TypesChangePasswordRequestDTO {
/**
* @type string
@@ -1343,21 +1310,6 @@ export interface TypesPostableAcceptInviteDTO {
token?: string;
}
export interface TypesPostableForgotPasswordDTO {
/**
* @type string
*/
email?: string;
/**
* @type string
*/
frontendBaseURL?: string;
/**
* @type string
*/
orgId?: string;
}
export interface TypesPostableInviteDTO {
/**
* @type string
@@ -1389,11 +1341,6 @@ export interface TypesPostableResetPasswordDTO {
}
export interface TypesResetPasswordTokenDTO {
/**
* @type string
* @format date-time
*/
expiresAt?: Date;
/**
* @type string
*/
@@ -1735,42 +1682,6 @@ export type GetPublicDashboardWidgetQueryRange200 = {
status?: string;
};
export type ListRoles200 = {
/**
* @type array
*/
data?: RoletypesRoleDTO[];
/**
* @type string
*/
status?: string;
};
export type CreateRole201 = {
data?: TypesIdentifiableDTO;
/**
* @type string
*/
status?: string;
};
export type DeleteRolePathParameters = {
id: string;
};
export type GetRolePathParameters = {
id: string;
};
export type GetRole200 = {
data?: RoletypesRoleDTO;
/**
* @type string
*/
status?: string;
};
export type PatchRolePathParameters = {
id: string;
};
export type ListUsers200 = {
/**
* @type array

View File

@@ -40,7 +40,6 @@ import type {
TypesChangePasswordRequestDTO,
TypesPostableAcceptInviteDTO,
TypesPostableAPIKeyDTO,
TypesPostableForgotPasswordDTO,
TypesPostableInviteDTO,
TypesPostableResetPasswordDTO,
TypesStorableAPIKeyDTO,
@@ -1568,87 +1567,3 @@ export const invalidateGetMyUser = async (
return queryClient;
};
/**
* This endpoint initiates the forgot password flow by sending a reset password email
* @summary Forgot password
*/
export const forgotPassword = (
typesPostableForgotPasswordDTO: TypesPostableForgotPasswordDTO,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/factor_password/forgot`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableForgotPasswordDTO,
signal,
});
};
export const getForgotPasswordMutationOptions = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof forgotPassword>>,
TError,
{ data: TypesPostableForgotPasswordDTO },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof forgotPassword>>,
TError,
{ data: TypesPostableForgotPasswordDTO },
TContext
> => {
const mutationKey = ['forgotPassword'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof forgotPassword>>,
{ data: TypesPostableForgotPasswordDTO }
> = (props) => {
const { data } = props ?? {};
return forgotPassword(data);
};
return { mutationFn, ...mutationOptions };
};
export type ForgotPasswordMutationResult = NonNullable<
Awaited<ReturnType<typeof forgotPassword>>
>;
export type ForgotPasswordMutationBody = TypesPostableForgotPasswordDTO;
export type ForgotPasswordMutationError = RenderErrorResponseDTO;
/**
* @summary Forgot password
*/
export const useForgotPassword = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof forgotPassword>>,
TError,
{ data: TypesPostableForgotPasswordDTO },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof forgotPassword>>,
TError,
{ data: TypesPostableForgotPasswordDTO },
TContext
> => {
const mutationOptions = getForgotPasswordMutationOptions(options);
return useMutation(mutationOptions);
};

View File

@@ -16,8 +16,8 @@
justify-content: center;
gap: 16px;
padding: 12px;
background: var(--bg-ink-400);
border: 1px solid var(--bg-ink-200);
background: var(--bg-ink-400, #121317);
border: 1px solid var(--bg-ink-200, #23262e);
border-radius: 4px;
}
@@ -49,7 +49,7 @@
font-size: 11px;
font-weight: 400;
line-height: 1;
color: var(--text-neutral-dark-100);
color: var(--text-neutral-dark-100, #adb4c2);
text-align: center;
}
@@ -67,7 +67,7 @@
.auth-footer-link-icon {
flex-shrink: 0;
color: var(--text-neutral-dark-50);
color: var(--text-neutral-dark-50, #eceef2);
}
.auth-footer-link-status {
@@ -84,14 +84,14 @@
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--bg-ink-200);
background: var(--bg-ink-200, #23262e);
flex-shrink: 0;
}
.lightMode {
.auth-footer-content {
background: var(--bg-base-white);
border-color: var(--bg-vanilla-300);
background: var(--bg-base-white, #ffffff);
border-color: var(--bg-vanilla-300, #e9e9e9);
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.08);
}
@@ -102,14 +102,14 @@
}
.auth-footer-text {
color: var(--text-neutral-light-200);
color: var(--text-neutral-light-200, #80828d);
}
.auth-footer-link-icon {
color: var(--text-neutral-light-100);
color: var(--text-neutral-light-100, #62636c);
}
.auth-footer-separator {
background: var(--bg-vanilla-300);
background: var(--bg-vanilla-300, #e9e9e9);
}
}

View File

@@ -30,7 +30,7 @@
font-size: 15.4px;
font-weight: 500;
line-height: 17.5px;
color: var(--text-neutral-dark-50);
color: var(--text-neutral-dark-50, #eceef2);
white-space: nowrap;
}
@@ -41,7 +41,7 @@
gap: 8px;
height: 32px;
padding: 10px 16px;
background: var(--bg-ink-400);
background: var(--bg-ink-400, #121317);
border: none;
border-radius: 2px;
cursor: pointer;
@@ -52,13 +52,13 @@
font-size: 11px;
font-weight: 500;
line-height: 1;
color: var(--text-neutral-dark-100);
color: var(--text-neutral-dark-100, #adb4c2);
text-align: center;
}
svg {
flex-shrink: 0;
color: var(--text-neutral-dark-100);
color: var(--text-neutral-dark-100, #adb4c2);
}
&:hover {
@@ -68,15 +68,15 @@
.lightMode {
.auth-header-logo-text {
color: var(--text-neutral-light-100);
color: var(--text-neutral-light-100, #62636c);
}
.auth-header-help-button {
background: var(--bg-vanilla-200);
background: var(--bg-vanilla-200, #f5f5f5);
span,
svg {
color: var(--text-neutral-light-200);
color: var(--text-neutral-light-200, #80828d);
}
}
}

View File

@@ -4,7 +4,7 @@
position: relative;
min-height: 100vh;
width: 100%;
background: var(--bg-neutral-dark-1000);
background: var(--bg-neutral-dark-1000, #0a0c10);
display: flex;
flex-direction: column;
}
@@ -33,7 +33,7 @@
.bg-dot-pattern {
background: radial-gradient(
circle,
var(--bg-neutral-dark-50) 1px,
var(--bg-neutral-dark-50, #eceef2) 1px,
transparent 1px
);
background-size: 12px 12px;
@@ -85,8 +85,8 @@
height: 100%;
background-image: repeating-linear-gradient(
to bottom,
var(--bg-ink-200) 0px,
var(--bg-ink-200) 4px,
var(--bg-ink-200, #23262e) 0px,
var(--bg-ink-200, #23262e) 4px,
transparent 4px,
transparent 8px
);
@@ -146,7 +146,7 @@
.lightMode {
.auth-page-wrapper {
background: var(--bg-base-white);
background: var(--bg-base-white, #ffffff);
}
.bg-dot-pattern {
@@ -172,8 +172,8 @@
.auth-page-line-right {
background-image: repeating-linear-gradient(
to bottom,
var(--bg-vanilla-300) 0px,
var(--bg-vanilla-300) 4px,
var(--bg-vanilla-300, #e9e9e9) 0px,
var(--bg-vanilla-300, #e9e9e9) 4px,
transparent 4px,
transparent 8px
);

View File

@@ -7,7 +7,6 @@ import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
import { clearPreviousQuery } from './QueryV2/previousQuery.utils';
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
import { QueryV2 } from './QueryV2/QueryV2';
import TraceOperator from './QueryV2/TraceOperator/TraceOperator';
@@ -25,7 +24,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
version,
onSignalSourceChange,
signalSourceChangeEnabled = false,
savePreviousQuery = false,
}: QueryBuilderProps): JSX.Element {
const {
currentQuery,
@@ -63,14 +61,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
newPanelType,
]);
useEffect(() => {
// always clear on mount and unmount to avoid stale data
clearPreviousQuery();
return (): void => {
clearPreviousQuery();
};
}, []);
const isMultiQueryAllowed = useMemo(
() => !isListViewPanel || showTraceOperator,
[showTraceOperator, isListViewPanel],
@@ -210,7 +200,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
signalSourceChangeEnabled={signalSourceChangeEnabled}
queriesCount={1}
savePreviousQuery={savePreviousQuery}
/>
) : (
currentQuery.builder.queryData.map((query, index) => (
@@ -233,7 +222,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
signalSourceChangeEnabled={signalSourceChangeEnabled}
queriesCount={currentQuery.builder.queryData.length}
savePreviousQuery={savePreviousQuery}
/>
))
)}

View File

@@ -13,13 +13,6 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
import {
getPreviousQueryFromKey,
getQueryKey,
removeKeyFromPreviousQuery,
saveAsPreviousQuery,
} from '../previousQuery.utils';
import './MetricsSelect.styles.scss';
export const SOURCE_OPTIONS: SelectOption<string, string>[] = [
@@ -34,7 +27,6 @@ export const MetricsSelect = memo(function MetricsSelect({
signalSource,
onSignalSourceChange,
signalSourceChangeEnabled = false,
savePreviousQuery = false,
}: {
query: IBuilderQuery;
index: number;
@@ -42,7 +34,6 @@ export const MetricsSelect = memo(function MetricsSelect({
signalSource: 'meter' | '';
onSignalSourceChange: (value: string) => void;
signalSourceChangeEnabled: boolean;
savePreviousQuery: boolean;
}): JSX.Element {
const [attributeKeys, setAttributeKeys] = useState<BaseAutocompleteData[]>([]);
@@ -59,11 +50,7 @@ export const MetricsSelect = memo(function MetricsSelect({
[handleChangeAggregatorAttribute, attributeKeys],
);
const {
updateAllQueriesOperators,
handleSetQueryData,
panelType,
} = useQueryBuilder();
const { updateAllQueriesOperators, handleSetQueryData } = useQueryBuilder();
const source = useMemo(
() => (signalSource === 'meter' ? 'meter' : 'metrics'),
@@ -92,63 +79,22 @@ export const MetricsSelect = memo(function MetricsSelect({
[updateAllQueriesOperators],
);
const getDefaultQueryFromSource = useCallback(
(selectedSource: string): IBuilderQuery => {
const isMeter = selectedSource === 'meter';
const baseQuery = isMeter
? defaultMeterQuery.builder.queryData[0]
: defaultMetricsQuery.builder.queryData[0];
return {
...baseQuery,
source: isMeter ? 'meter' : '',
queryName: query.queryName,
};
},
[defaultMeterQuery, defaultMetricsQuery, query.queryName],
);
const handleSignalSourceChange = (value: string): void => {
let newQueryData: IBuilderQuery;
if (savePreviousQuery) {
const queryName = query.queryName || '';
const dataSource = query.dataSource || '';
const currSignalSource = query.source ?? '';
const newSignalSource = value === 'meter' ? 'meter' : '';
const currQueryKey = getQueryKey({
queryName: queryName,
dataSource: dataSource,
signalSource: currSignalSource,
panelType: panelType || '',
});
// save the current query key in session storage
saveAsPreviousQuery(currQueryKey, query);
const newQueryKey = getQueryKey({
queryName: queryName,
dataSource: dataSource,
signalSource: newSignalSource,
panelType: panelType || '',
});
const savedQuery: IBuilderQuery | null = getPreviousQueryFromKey(
newQueryKey,
);
// remove the new query key from session storage
removeKeyFromPreviousQuery(newQueryKey);
newQueryData = savedQuery
? savedQuery
: getDefaultQueryFromSource(newSignalSource);
} else {
newQueryData = getDefaultQueryFromSource(value);
}
onSignalSourceChange(value);
handleSetQueryData(index, newQueryData);
handleSetQueryData(
index,
value === 'meter'
? {
...defaultMeterQuery.builder.queryData[0],
source: 'meter',
queryName: query.queryName,
}
: {
...defaultMetricsQuery.builder.queryData[0],
source: '',
queryName: query.queryName,
},
);
};
return (

View File

@@ -42,12 +42,10 @@ export const QueryV2 = forwardRef(function QueryV2(
onSignalSourceChange,
signalSourceChangeEnabled = false,
queriesCount = 1,
savePreviousQuery = false,
}: QueryProps & {
onSignalSourceChange: (value: string) => void;
signalSourceChangeEnabled: boolean;
queriesCount: number;
savePreviousQuery: boolean;
},
ref: ForwardedRef<HTMLDivElement>,
): JSX.Element {
@@ -69,7 +67,6 @@ export const QueryV2 = forwardRef(function QueryV2(
filterConfigs,
isListViewPanel,
entityVersion: version,
savePreviousQuery,
});
const handleToggleDisableQuery = useCallback(() => {
@@ -237,7 +234,6 @@ export const QueryV2 = forwardRef(function QueryV2(
signalSource={signalSource as 'meter' | ''}
onSignalSourceChange={onSignalSourceChange}
signalSourceChangeEnabled={signalSourceChangeEnabled}
savePreviousQuery={savePreviousQuery}
/>
</div>
)}

View File

@@ -11,14 +11,9 @@ import {
IBuilderQuery,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { QueryFunction } from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { UseQueryOperations } from 'types/common/operations.types';
import {
DataSource,
QueryBuilderContextType,
QueryFunctionsTypes,
} from 'types/common/queryBuilder';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
import '@testing-library/jest-dom';
@@ -50,17 +45,12 @@ const mockedUseQueryOperations = jest.mocked(
describe('QueryBuilderV2 + QueryV2 - base render', () => {
let handleRunQueryMock: jest.MockedFunction<() => void>;
let handleQueryFunctionsUpdatesMock: jest.MockedFunction<() => void>;
let baseQBContext: QueryBuilderContextType;
beforeEach(() => {
const mockCloneQuery = jest.fn() as jest.MockedFunction<
(type: string, q: IBuilderQuery) => void
>;
handleRunQueryMock = jest.fn() as jest.MockedFunction<() => void>;
handleQueryFunctionsUpdatesMock = jest.fn() as jest.MockedFunction<
() => void
>;
const baseQuery: IBuilderQuery = {
queryName: 'A',
dataSource: DataSource.LOGS,
@@ -101,7 +91,7 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
const updateQueriesData: QueryBuilderContextType['updateQueriesData'] = (q) =>
q;
const baseContext = ({
mockedUseQueryBuilder.mockReturnValue(({
currentQuery: currentQueryObj,
stagedQuery: null,
lastUsedQuery: null,
@@ -134,10 +124,7 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
initQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(() => false),
isDefaultQuery: jest.fn(() => false),
} as unknown) as QueryBuilderContextType;
baseQBContext = baseContext;
mockedUseQueryBuilder.mockReturnValue(baseQBContext);
} as unknown) as QueryBuilderContextType);
mockedUseQueryOperations.mockReturnValue({
isTracePanelType: false,
@@ -152,7 +139,7 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
handleDeleteQuery: jest.fn(),
handleChangeQueryData: (jest.fn() as unknown) as ReturnType<UseQueryOperations>['handleChangeQueryData'],
handleChangeFormulaData: jest.fn(),
handleQueryFunctionsUpdates: handleQueryFunctionsUpdatesMock,
handleQueryFunctionsUpdates: jest.fn(),
listOfAdditionalFormulaFilters: [],
});
});
@@ -212,56 +199,4 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
expect(handleRunQueryMock).toHaveBeenCalled();
});
it('fx button is disabled when functions already exist', () => {
const currentQueryBase = baseQBContext.currentQuery as Query;
const supersetQueryBase = baseQBContext.supersetQuery as Query;
mockedUseQueryBuilder.mockReturnValueOnce({
...baseQBContext,
currentQuery: {
...currentQueryBase,
builder: {
...currentQueryBase.builder,
queryData: [
{
...currentQueryBase.builder.queryData[0],
functions: [
{ name: QueryFunctionsTypes.TIME_SHIFT, args: [] } as QueryFunction,
],
},
],
},
},
supersetQuery: {
...supersetQueryBase,
builder: {
...supersetQueryBase.builder,
queryData: [
{
...supersetQueryBase.builder.queryData[0],
functions: [
{ name: QueryFunctionsTypes.TIME_SHIFT, args: [] } as QueryFunction,
],
},
],
},
},
});
render(<QueryBuilderV2 panelType={PANEL_TYPES.TABLE} version="v4" />);
const fxButton = document.querySelector('.function-btn') as HTMLButtonElement;
expect(fxButton).toBeInTheDocument();
expect(fxButton).toBeDisabled();
const deleteButton = document.querySelector(
'.query-function-delete-btn',
) as HTMLButtonElement;
expect(deleteButton).toBeInTheDocument();
userEvent.click(deleteButton);
waitFor(() => {
expect(fxButton).not.toBeDisabled();
});
});
});

View File

@@ -1,70 +0,0 @@
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export const PREVIOUS_QUERY_KEY = 'previousQuery';
function getPreviousQueryFromStore(): Record<string, IBuilderQuery> {
try {
const raw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
if (!raw) {
return {};
}
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}
function writePreviousQueryToStore(store: Record<string, IBuilderQuery>): void {
try {
sessionStorage.setItem(PREVIOUS_QUERY_KEY, JSON.stringify(store));
} catch {
// ignore quota or serialization errors
}
}
export const getQueryKey = ({
queryName,
dataSource,
signalSource,
panelType,
}: {
queryName: string;
dataSource: string;
signalSource: string;
panelType: string;
}): string => {
const qn = queryName || '';
const ds = dataSource || '';
const ss = signalSource === 'meter' ? 'meter' : '';
const pt = panelType || '';
return `${qn}:${ds}:${ss}:${pt}`;
};
export const getPreviousQueryFromKey = (key: string): IBuilderQuery | null => {
const previousQuery = getPreviousQueryFromStore();
return previousQuery?.[key] ?? null;
};
export const saveAsPreviousQuery = (
key: string,
query: IBuilderQuery,
): void => {
const previousQuery = getPreviousQueryFromStore();
previousQuery[key] = query;
writePreviousQueryToStore(previousQuery);
};
export const removeKeyFromPreviousQuery = (key: string): void => {
const previousQuery = getPreviousQueryFromStore();
delete previousQuery[key];
writePreviousQueryToStore(previousQuery);
};
export const clearPreviousQuery = (): void => {
try {
sessionStorage.removeItem(PREVIOUS_QUERY_KEY);
} catch {
// no-op
}
};

View File

@@ -1,369 +0,0 @@
import { jest } from '@jest/globals';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { render, screen, userEvent } from 'tests/test-utils';
import {
Having,
IBuilderQuery,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import '@testing-library/jest-dom';
import { QueryBuilderV2 } from '../QueryBuilderV2';
import {
clearPreviousQuery,
PREVIOUS_QUERY_KEY,
} from '../QueryV2/previousQuery.utils';
// Local mocks for domain-specific heavy child components
jest.mock(
'../QueryV2/QueryAggregation/QueryAggregation',
() =>
function QueryAggregation(): JSX.Element {
return <div>QueryAggregation</div>;
},
);
jest.mock(
'../QueryV2/MerticsAggregateSection/MetricsAggregateSection',
() =>
function MetricsAggregateSection(): JSX.Element {
return <div>MetricsAggregateSection</div>;
},
);
// Mock networked children to avoid axios during unit tests
jest.mock(
'../QueryV2/QuerySearch/QuerySearch',
() =>
function QuerySearch(): JSX.Element {
return <div>QuerySearch</div>;
},
);
jest.mock('container/QueryBuilder/filters', () => ({
AggregatorFilter: (): JSX.Element => <div />,
}));
// Mock hooks
jest.mock('hooks/queryBuilder/useQueryBuilder');
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
describe('MetricsSelect - signal source switching (standalone)', () => {
let handleSetQueryDataMock: jest.MockedFunction<
(index: number, q: IBuilderQuery) => void
>;
beforeEach(() => {
clearPreviousQuery();
handleSetQueryDataMock = (jest.fn() as unknown) as jest.MockedFunction<
(index: number, q: IBuilderQuery) => void
>;
const metricsQuery: IBuilderQuery = {
queryName: 'A',
dataSource: DataSource.METRICS,
aggregateOperator: '',
aggregations: [
{
timeAggregation: '',
metricName: 'test_metric',
temporality: '',
spaceAggregation: '',
},
],
timeAggregation: '',
spaceAggregation: '',
temporality: '',
functions: [],
filter: {
expression: 'service = "test"',
},
filters: { items: [], op: 'AND' },
groupBy: [],
expression: '',
disabled: false,
having: [] as Having[],
limit: 10,
stepInterval: null,
orderBy: [],
legend: 'A',
source: '',
};
const currentQueryObj: Query = {
id: 'test',
unit: undefined,
queryType: EQueryType.CLICKHOUSE,
promql: [],
clickhouse_sql: [],
builder: {
queryData: [metricsQuery],
queryFormulas: [],
queryTraceOperator: [],
},
};
(mockedUseQueryBuilder as any).mockReturnValue({
currentQuery: currentQueryObj,
stagedQuery: null,
lastUsedQuery: null,
setLastUsedQuery: jest.fn(),
supersetQuery: currentQueryObj,
setSupersetQuery: jest.fn(),
initialDataSource: null,
panelType: PANEL_TYPES.TABLE,
isEnabledQuery: true,
handleSetQueryData: handleSetQueryDataMock,
handleSetTraceOperatorData: jest.fn(),
handleSetFormulaData: jest.fn(),
handleSetQueryItemData: jest.fn(),
handleSetConfig: jest.fn(),
removeQueryBuilderEntityByIndex: jest.fn(),
removeAllQueryBuilderEntities: jest.fn(),
removeQueryTypeItemByIndex: jest.fn(),
addNewBuilderQuery: jest.fn(),
addNewFormula: jest.fn(),
removeTraceOperator: jest.fn(),
addTraceOperator: jest.fn(),
cloneQuery: jest.fn(),
addNewQueryItem: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
handleRunQuery: jest.fn(),
resetQuery: jest.fn(),
handleOnUnitsChange: jest.fn(),
updateAllQueriesOperators: ((q: any) => q) as any,
updateQueriesData: ((q: any) => q) as any,
initQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(() => false),
isDefaultQuery: jest.fn(() => false),
});
});
afterEach(() => {
jest.clearAllMocks();
clearPreviousQuery();
});
it('savePreviousQuery=true: metrics → meter saves previous query in session storage with appropriate key and query', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<QueryBuilderV2
panelType={PANEL_TYPES.TABLE}
version="v5"
signalSourceChangeEnabled
savePreviousQuery
/>,
);
const trigger = document.querySelector(
'.metrics-container .source-selector .ant-select-selector',
) as HTMLElement;
await user.click(trigger);
// wait for dropdown and choose Meter
const meterOption = await screen.findByText('Meter');
await user.click(meterOption);
expect(handleSetQueryDataMock).toHaveBeenCalled();
const [, arg] = handleSetQueryDataMock.mock.calls[0];
expect(arg.queryName).toBe('A');
expect(arg.dataSource).toBe(DataSource.METRICS);
expect(arg.source).toBe('meter');
// verify previousQuery store has the expected key and filter expression
const storeRaw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
expect(storeRaw).not.toBeNull();
const store = JSON.parse(storeRaw || '{}') as Record<string, IBuilderQuery>;
expect(Object.keys(store)).toContain('A:metrics::table');
expect(store['A:metrics::table']?.filter?.expression).toBe(
'service = "test"',
);
});
it('savePreviousQuery=false: metrics → meter does not write to sessionStorage and applies defaults', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
// render WITHOUT savePreviousQuery enabled
render(
<QueryBuilderV2
panelType={PANEL_TYPES.TABLE}
version="v5"
signalSourceChangeEnabled
/>,
);
// open Source and choose Meter
const trigger = document.querySelector(
'.metrics-container .source-selector .ant-select-selector',
) as HTMLElement;
await user.click(trigger);
const meterOption = await screen.findByText('Meter');
await user.click(meterOption);
// assert no session storage written
const storeRaw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
// either null or empty object string
expect(storeRaw === null || storeRaw === '{}').toBe(true);
// and query updated to defaults (at least source should be 'meter')
expect(handleSetQueryDataMock).toHaveBeenCalled();
const [, arg] = handleSetQueryDataMock.mock.calls[0];
expect(arg.queryName).toBe('A');
expect(arg.dataSource).toBe(DataSource.METRICS);
expect(arg.source).toBe('meter');
});
});
describe('DataSource change - Logs to Traces', () => {
let handleSetQueryDataMock: jest.MockedFunction<
(index: number, q: IBuilderQuery) => void
>;
beforeEach(() => {
clearPreviousQuery();
handleSetQueryDataMock = (jest.fn() as unknown) as jest.MockedFunction<
(i: number, q: IBuilderQuery) => void
>;
const logsQuery: IBuilderQuery = {
queryName: 'A',
dataSource: DataSource.LOGS,
aggregateOperator: '',
aggregations: [],
timeAggregation: '',
spaceAggregation: '',
temporality: '',
functions: [],
filter: { expression: 'body CONTAINS "error"' },
filters: { items: [], op: 'AND' },
groupBy: [],
expression: '',
disabled: false,
having: [] as Having[],
limit: 100,
stepInterval: null,
orderBy: [],
legend: 'L',
source: '',
};
const logsCurrentQuery: Query = {
id: 'test-logs',
unit: undefined,
queryType: EQueryType.CLICKHOUSE,
promql: [],
clickhouse_sql: [],
builder: {
queryData: [logsQuery],
queryFormulas: [],
queryTraceOperator: [],
},
};
mockedUseQueryBuilder.mockReset();
mockedUseQueryBuilder.mockReturnValue({
currentQuery: logsCurrentQuery,
stagedQuery: null,
lastUsedQuery: null,
setLastUsedQuery: jest.fn(),
supersetQuery: logsCurrentQuery,
setSupersetQuery: jest.fn(),
initialDataSource: null,
panelType: PANEL_TYPES.TABLE,
isEnabledQuery: true,
handleSetQueryData: handleSetQueryDataMock,
handleSetTraceOperatorData: jest.fn(),
handleSetFormulaData: jest.fn(),
handleSetQueryItemData: jest.fn(),
handleSetConfig: jest.fn(),
removeQueryBuilderEntityByIndex: jest.fn(),
removeAllQueryBuilderEntities: jest.fn(),
removeQueryTypeItemByIndex: jest.fn(),
addNewBuilderQuery: jest.fn(),
addNewFormula: jest.fn(),
removeTraceOperator: jest.fn(),
addTraceOperator: jest.fn(),
cloneQuery: jest.fn(),
addNewQueryItem: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
handleRunQuery: jest.fn(),
resetQuery: jest.fn(),
handleOnUnitsChange: jest.fn(),
updateAllQueriesOperators: ((q: any) => q) as any,
updateQueriesData: ((q: any) => q) as any,
initQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(() => false),
isDefaultQuery: jest.fn(() => false),
});
});
afterEach(() => {
jest.clearAllMocks();
clearPreviousQuery();
});
it('updates query dataSource to TRACES saves previous query in session storage', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<QueryBuilderV2
panelType={PANEL_TYPES.TABLE}
version="v5"
signalSourceChangeEnabled
savePreviousQuery
/>,
);
const logsTrigger = document.querySelector(
'.query-data-source .ant-select-selector',
) as HTMLElement;
await user.click(logsTrigger);
const tracesContent = screen.getByText('Traces', {
selector: '.ant-select-item-option-content',
});
// click the option (prefer the option element; fallback to content)
await user.click(
(tracesContent.closest('[role="option"]') as HTMLElement) || tracesContent,
);
// verify previousQuery store saved the current LOGS snapshot before switch
const storeRaw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
expect(storeRaw).not.toBeNull();
const store = JSON.parse(storeRaw || '{}') as Record<string, IBuilderQuery>;
expect(Object.keys(store)).toContain('A:logs::table');
expect(store['A:logs::table']?.filter?.expression).toBe(
'body CONTAINS "error"',
);
});
it('updates query dataSource to TRACES does not saves previous query in session storage', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<QueryBuilderV2
panelType={PANEL_TYPES.TABLE}
version="v5"
signalSourceChangeEnabled
/>,
);
const logsTrigger = document.querySelector(
'.query-data-source .ant-select-selector',
) as HTMLElement;
await user.click(logsTrigger);
const tracesContent = screen.getByText('Traces', {
selector: '.ant-select-item-option-content',
});
// click the option (prefer the option element; fallback to content)
await user.click(
(tracesContent.closest('[role="option"]') as HTMLElement) || tracesContent,
);
// Assert that no snapshot was written
const storeRaw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
expect(storeRaw === null || storeRaw === '{}').toBe(true);
});
});

View File

@@ -1,173 +0,0 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import '@testing-library/jest-dom';
import {
clearPreviousQuery,
getPreviousQueryFromKey,
getQueryKey,
PREVIOUS_QUERY_KEY,
removeKeyFromPreviousQuery,
saveAsPreviousQuery,
} from '../QueryV2/previousQuery.utils';
describe('previousQuery.utils', () => {
const sampleQuery: IBuilderQuery = {
queryName: 'A',
dataSource: 'metrics' as any,
aggregateOperator: '',
aggregations: [],
timeAggregation: '',
spaceAggregation: '',
temporality: '',
functions: [],
filter: { expression: 'service = "test"' },
filters: { items: [], op: 'AND' },
groupBy: [],
expression: '',
disabled: false,
having: [],
limit: 10,
stepInterval: null,
orderBy: [],
legend: 'A',
source: '',
};
beforeEach(() => {
try {
sessionStorage.clear();
} catch {
// jsdom environment
}
});
afterEach(() => {
jest.restoreAllMocks();
});
it('getQueryKey normalizes non-meter signal to empty string', () => {
const k1 = getQueryKey({
queryName: 'A',
dataSource: 'metrics',
signalSource: 'metrics', // should normalize to ''
panelType: 'TABLE',
});
const k2 = getQueryKey({
queryName: 'A',
dataSource: 'metrics',
signalSource: '',
panelType: 'TABLE',
});
expect(k1).toBe('A:metrics::TABLE');
expect(k2).toBe('A:metrics::TABLE');
const km = getQueryKey({
queryName: 'A',
dataSource: 'metrics',
signalSource: 'meter',
panelType: 'TABLE',
});
expect(km).toBe('A:metrics:meter:TABLE');
});
it('returns null for missing key when store is empty', () => {
expect(getPreviousQueryFromKey('missing:key')).toBeNull();
});
it('saveAsPreviousQuery writes and getPreviousQueryFromKey reads the same object', () => {
const key = getQueryKey({
queryName: 'A',
dataSource: 'metrics',
signalSource: '',
panelType: 'TABLE',
});
saveAsPreviousQuery(key, sampleQuery);
const fromStore = getPreviousQueryFromKey(key);
expect(fromStore).toEqual(sampleQuery);
});
it('saveAsPreviousQuery merges multiple entries and removeKeyFromPreviousQuery deletes one', () => {
const k1 = getQueryKey({
queryName: 'A',
dataSource: 'metrics',
signalSource: '',
panelType: 'TABLE',
});
const k2 = getQueryKey({
queryName: 'B',
dataSource: 'metrics',
signalSource: 'meter',
panelType: 'TABLE',
});
saveAsPreviousQuery(k1, sampleQuery);
saveAsPreviousQuery(k2, {
...sampleQuery,
queryName: 'B',
source: 'meter' as any,
});
expect(getPreviousQueryFromKey(k1)?.queryName).toBe('A');
expect(getPreviousQueryFromKey(k2)?.queryName).toBe('B');
removeKeyFromPreviousQuery(k1);
expect(getPreviousQueryFromKey(k1)).toBeNull();
expect(getPreviousQueryFromKey(k2)?.queryName).toBe('B');
});
it('clearPreviousQuery removes the store key', () => {
const key = getQueryKey({
queryName: 'A',
dataSource: 'metrics',
signalSource: '',
panelType: 'TABLE',
});
saveAsPreviousQuery(key, sampleQuery);
expect(sessionStorage.getItem(PREVIOUS_QUERY_KEY)).not.toBeNull();
clearPreviousQuery();
expect(sessionStorage.getItem(PREVIOUS_QUERY_KEY)).toBeNull();
});
it('handles malformed JSON in store gracefully', () => {
sessionStorage.setItem(PREVIOUS_QUERY_KEY, 'not valid json');
// Should not throw and behave as empty store
expect(getPreviousQueryFromKey('any:key')).toBeNull();
// After a save, it should overwrite the bad value with valid JSON
const validKey = getQueryKey({
queryName: 'A',
dataSource: 'metrics',
signalSource: '',
panelType: 'TABLE',
});
expect(() => saveAsPreviousQuery(validKey, sampleQuery)).not.toThrow();
const parsed = JSON.parse(sessionStorage.getItem(PREVIOUS_QUERY_KEY) || '{}');
expect(parsed[validKey]).toBeTruthy();
});
it('write errors (e.g., quota) are caught and do not throw', () => {
const spy = jest
.spyOn(window.sessionStorage.__proto__, 'setItem')
.mockImplementation(() => {
throw new Error('quota exceeded');
});
const key = getQueryKey({
queryName: 'A',
dataSource: 'metrics',
signalSource: '',
panelType: 'TABLE',
});
expect(() => saveAsPreviousQuery(key, sampleQuery)).not.toThrow();
// Since write failed, reading should still behave as empty
spy.mockRestore();
expect(getPreviousQueryFromKey(key)).toBeNull();
});
});

View File

@@ -54,5 +54,4 @@ export enum QueryParams {
version = 'version',
source = 'source',
showClassicCreateAlertsPage = 'showClassicCreateAlertsPage',
isTestAlert = 'isTestAlert',
}

View File

@@ -48,7 +48,7 @@
}
.app-content {
width: calc(100% - 54px); // width of the sidebar
width: calc(100% - 64px); // width of the sidebar
z-index: 0;
margin: 0 auto;

View File

@@ -338,7 +338,7 @@ describe('CreateAlertV2 utils', () => {
const props = getCreateAlertLocalStateFromAlertDef(args);
expect(props).toBeDefined();
expect(props).toMatchObject({
basic: {
basicAlertState: {
...INITIAL_ALERT_STATE,
name: 'test-alert',
labels: {
@@ -348,10 +348,10 @@ describe('CreateAlertV2 utils', () => {
yAxisUnit: UniversalYAxisUnit.MINUTES,
},
// as we have already verified these utils in their respective tests
threshold: expect.any(Object),
advancedOptions: expect.any(Object),
evaluationWindow: expect.any(Object),
notificationSettings: expect.any(Object),
thresholdState: expect.any(Object),
advancedOptionsState: expect.any(Object),
evaluationWindowState: expect.any(Object),
notificationSettingsState: expect.any(Object),
});
});
});

View File

@@ -196,11 +196,3 @@ export const INITIAL_NOTIFICATION_SETTINGS_STATE: NotificationSettingsState = {
description: NOTIFICATION_MESSAGE_PLACEHOLDER,
routingPolicies: false,
};
export const INITIAL_CREATE_ALERT_STATE = {
basic: INITIAL_ALERT_STATE,
threshold: INITIAL_ALERT_THRESHOLD_STATE,
advancedOptions: INITIAL_ADVANCED_OPTIONS_STATE,
evaluationWindow: INITIAL_EVALUATION_WINDOW_STATE,
notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE,
};

View File

@@ -17,22 +17,26 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { INITIAL_CREATE_ALERT_STATE } from './constants';
import {
AdvancedOptionsAction,
AlertThresholdAction,
INITIAL_ADVANCED_OPTIONS_STATE,
INITIAL_ALERT_STATE,
INITIAL_ALERT_THRESHOLD_STATE,
INITIAL_EVALUATION_WINDOW_STATE,
INITIAL_NOTIFICATION_SETTINGS_STATE,
} from './constants';
import {
AlertThresholdMatchType,
CreateAlertAction,
CreateAlertSlice,
EvaluationWindowAction,
ICreateAlertContextProps,
ICreateAlertProviderProps,
NotificationSettingsAction,
} from './types';
import {
advancedOptionsReducer,
alertCreationReducer,
alertThresholdReducer,
buildInitialAlertDef,
createAlertReducer,
evaluationWindowReducer,
getInitialAlertTypeFromURL,
notificationSettingsReducer,
} from './utils';
const CreateAlertContext = createContext<ICreateAlertContextProps | null>(null);
@@ -61,65 +65,10 @@ export function CreateAlertProvider(
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const [createAlertState, setCreateAlertState] = useReducer(
createAlertReducer,
{
...INITIAL_CREATE_ALERT_STATE,
basic: {
...INITIAL_CREATE_ALERT_STATE.basic,
yAxisUnit: currentQuery.unit,
},
},
);
const setAlertState = useCallback(
(action: CreateAlertAction) => {
setCreateAlertState({
slice: CreateAlertSlice.BASIC,
action,
});
},
[setCreateAlertState],
);
const setThresholdState = useCallback(
(action: AlertThresholdAction) => {
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
action,
});
},
[setCreateAlertState],
);
const setEvaluationWindow = useCallback(
(action: EvaluationWindowAction) => {
setCreateAlertState({
slice: CreateAlertSlice.EVALUATION_WINDOW,
action,
});
},
[setCreateAlertState],
);
const setAdvancedOptions = useCallback(
(action: AdvancedOptionsAction) => {
setCreateAlertState({
slice: CreateAlertSlice.ADVANCED_OPTIONS,
action,
});
},
[setCreateAlertState],
);
const setNotificationSettings = useCallback(
(action: NotificationSettingsAction) => {
setCreateAlertState({
slice: CreateAlertSlice.NOTIFICATION_SETTINGS,
action,
});
},
[setCreateAlertState],
);
const [alertState, setAlertState] = useReducer(alertCreationReducer, {
...INITIAL_ALERT_STATE,
yAxisUnit: currentQuery.unit,
});
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
@@ -155,56 +104,92 @@ export function CreateAlertProvider(
[redirectWithQueryBuilderData],
);
const [thresholdState, setThresholdState] = useReducer(
alertThresholdReducer,
INITIAL_ALERT_THRESHOLD_STATE,
);
const [evaluationWindow, setEvaluationWindow] = useReducer(
evaluationWindowReducer,
INITIAL_EVALUATION_WINDOW_STATE,
);
const [advancedOptions, setAdvancedOptions] = useReducer(
advancedOptionsReducer,
INITIAL_ADVANCED_OPTIONS_STATE,
);
const [notificationSettings, setNotificationSettings] = useReducer(
notificationSettingsReducer,
INITIAL_NOTIFICATION_SETTINGS_STATE,
);
useEffect(() => {
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
action: {
type: 'RESET',
},
setThresholdState({
type: 'RESET',
});
if (thresholdsFromURL) {
try {
const thresholds = JSON.parse(thresholdsFromURL);
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
action: {
type: 'SET_THRESHOLDS',
payload: thresholds,
},
setThresholdState({
type: 'SET_THRESHOLDS',
payload: thresholds,
});
} catch (error) {
console.error('Error parsing thresholds from URL:', error);
}
setCreateAlertState({
slice: CreateAlertSlice.EVALUATION_WINDOW,
action: {
type: 'SET_INITIAL_STATE_FOR_METER',
},
setEvaluationWindow({
type: 'SET_INITIAL_STATE_FOR_METER',
});
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
action: {
type: 'SET_MATCH_TYPE',
payload: AlertThresholdMatchType.IN_TOTAL,
},
setThresholdState({
type: 'SET_MATCH_TYPE',
payload: AlertThresholdMatchType.IN_TOTAL,
});
}
}, [alertType, thresholdsFromURL]);
useEffect(() => {
if (isEditMode && initialAlertState) {
setCreateAlertState({
setAlertState({
type: 'SET_INITIAL_STATE',
payload: initialAlertState,
payload: initialAlertState.basicAlertState,
});
setThresholdState({
type: 'SET_INITIAL_STATE',
payload: initialAlertState.thresholdState,
});
setEvaluationWindow({
type: 'SET_INITIAL_STATE',
payload: initialAlertState.evaluationWindowState,
});
setAdvancedOptions({
type: 'SET_INITIAL_STATE',
payload: initialAlertState.advancedOptionsState,
});
setNotificationSettings({
type: 'SET_INITIAL_STATE',
payload: initialAlertState.notificationSettingsState,
});
}
}, [initialAlertState, isEditMode]);
const discardAlertRule = useCallback(() => {
setCreateAlertState({
setAlertState({
type: 'RESET',
});
setThresholdState({
type: 'RESET',
});
setEvaluationWindow({
type: 'RESET',
});
setAdvancedOptions({
type: 'RESET',
});
setNotificationSettings({
type: 'RESET',
});
handleAlertTypeChange(AlertTypes.METRICS_BASED_ALERT);
@@ -227,17 +212,17 @@ export function CreateAlertProvider(
const contextValue: ICreateAlertContextProps = useMemo(
() => ({
alertState: createAlertState.basic,
alertState,
setAlertState,
alertType,
setAlertType: handleAlertTypeChange,
thresholdState: createAlertState.threshold,
thresholdState,
setThresholdState,
evaluationWindow: createAlertState.evaluationWindow,
evaluationWindow,
setEvaluationWindow,
advancedOptions: createAlertState.advancedOptions,
advancedOptions,
setAdvancedOptions,
notificationSettings: createAlertState.notificationSettings,
notificationSettings,
setNotificationSettings,
discardAlertRule,
createAlertRule,
@@ -249,14 +234,13 @@ export function CreateAlertProvider(
isEditMode: isEditMode || false,
}),
[
createAlertState,
setAlertState,
setThresholdState,
setEvaluationWindow,
setAdvancedOptions,
setNotificationSettings,
alertState,
alertType,
handleAlertTypeChange,
thresholdState,
evaluationWindow,
advancedOptions,
notificationSettings,
discardAlertRule,
createAlertRule,
isCreatingAlertRule,

View File

@@ -9,6 +9,8 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
import { Labels } from 'types/api/alerts/def';
import { GetCreateAlertLocalStateFromAlertDefReturn } from '../types';
export interface ICreateAlertContextProps {
alertState: AlertState;
setAlertState: Dispatch<CreateAlertAction>;
@@ -50,7 +52,7 @@ export interface ICreateAlertContextProps {
export interface ICreateAlertProviderProps {
children: React.ReactNode;
initialAlertType: AlertTypes;
initialAlertState?: CreateAlertState;
initialAlertState?: GetCreateAlertLocalStateFromAlertDefReturn;
isEditMode?: boolean;
ruleId?: string;
}
@@ -270,31 +272,3 @@ export type NotificationSettingsAction =
| { type: 'SET_ROUTING_POLICIES'; payload: boolean }
| { type: 'SET_INITIAL_STATE'; payload: NotificationSettingsState }
| { type: 'RESET' };
export type CreateAlertState = {
basic: AlertState;
threshold: AlertThresholdState;
advancedOptions: AdvancedOptionsState;
evaluationWindow: EvaluationWindowState;
notificationSettings: NotificationSettingsState;
};
export enum CreateAlertSlice {
BASIC = 'basic',
THRESHOLD = 'threshold',
ADVANCED_OPTIONS = 'advancedOptions',
EVALUATION_WINDOW = 'evaluationWindow',
NOTIFICATION_SETTINGS = 'notificationSettings',
}
export type CreateAlertReducerAction =
| { slice: CreateAlertSlice.BASIC; action: CreateAlertAction }
| { slice: CreateAlertSlice.THRESHOLD; action: AlertThresholdAction }
| { slice: CreateAlertSlice.ADVANCED_OPTIONS; action: AdvancedOptionsAction }
| { slice: CreateAlertSlice.EVALUATION_WINDOW; action: EvaluationWindowAction }
| {
slice: CreateAlertSlice.NOTIFICATION_SETTINGS;
action: NotificationSettingsAction;
}
| { type: 'RESET' }
| { type: 'SET_INITIAL_STATE'; payload: CreateAlertState };

View File

@@ -18,7 +18,6 @@ import {
INITIAL_ADVANCED_OPTIONS_STATE,
INITIAL_ALERT_STATE,
INITIAL_ALERT_THRESHOLD_STATE,
INITIAL_CREATE_ALERT_STATE,
INITIAL_EVALUATION_WINDOW_STATE,
INITIAL_NOTIFICATION_SETTINGS_STATE,
} from './constants';
@@ -29,9 +28,6 @@ import {
AlertThresholdAction,
AlertThresholdState,
CreateAlertAction,
CreateAlertReducerAction,
CreateAlertSlice,
CreateAlertState,
EvaluationWindowAction,
EvaluationWindowState,
NotificationSettingsAction,
@@ -255,57 +251,3 @@ export const notificationSettingsReducer = (
return state;
}
};
export const createAlertReducer = (
state: CreateAlertState,
action: CreateAlertReducerAction,
): CreateAlertState => {
// Global actions
if ('type' in action) {
switch (action.type) {
case 'RESET':
return INITIAL_CREATE_ALERT_STATE;
case 'SET_INITIAL_STATE':
return action.payload;
default:
return state;
}
}
// Slice actions
switch (action.slice) {
case CreateAlertSlice.BASIC:
return { ...state, basic: alertCreationReducer(state.basic, action.action) };
case CreateAlertSlice.THRESHOLD:
return {
...state,
threshold: alertThresholdReducer(state.threshold, action.action),
};
case CreateAlertSlice.ADVANCED_OPTIONS:
return {
...state,
advancedOptions: advancedOptionsReducer(
state.advancedOptions,
action.action,
),
};
case CreateAlertSlice.EVALUATION_WINDOW:
return {
...state,
evaluationWindow: evaluationWindowReducer(
state.evaluationWindow,
action.action,
),
};
case CreateAlertSlice.NOTIFICATION_SETTINGS:
return {
...state,
notificationSettings: notificationSettingsReducer(
state.notificationSettings,
action.action,
),
};
default:
return state;
}
};

View File

@@ -21,11 +21,11 @@ import {
AlertThresholdMatchType,
AlertThresholdOperator,
AlertThresholdState,
CreateAlertState,
EvaluationWindowState,
NotificationSettingsState,
} from './context/types';
import { EVALUATION_WINDOW_TIMEFRAME } from './EvaluationSettings/constants';
import { GetCreateAlertLocalStateFromAlertDefReturn } from './types';
export function Spinner(): JSX.Element | null {
const { isCreatingAlertRule, isUpdatingAlertRule } = useCreateAlertState();
@@ -265,14 +265,14 @@ export function getThresholdStateFromAlertDef(
export function getCreateAlertLocalStateFromAlertDef(
alertDef: PostableAlertRuleV2 | undefined,
): CreateAlertState {
): GetCreateAlertLocalStateFromAlertDefReturn {
if (!alertDef) {
return {
basic: INITIAL_ALERT_STATE,
threshold: INITIAL_ALERT_THRESHOLD_STATE,
advancedOptions: INITIAL_ADVANCED_OPTIONS_STATE,
evaluationWindow: INITIAL_EVALUATION_WINDOW_STATE,
notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE,
basicAlertState: INITIAL_ALERT_STATE,
thresholdState: INITIAL_ALERT_THRESHOLD_STATE,
advancedOptionsState: INITIAL_ADVANCED_OPTIONS_STATE,
evaluationWindowState: INITIAL_EVALUATION_WINDOW_STATE,
notificationSettingsState: INITIAL_NOTIFICATION_SETTINGS_STATE,
};
}
// Basic alert state
@@ -294,10 +294,10 @@ export function getCreateAlertLocalStateFromAlertDef(
);
return {
basic: basicAlertState,
threshold: thresholdState,
advancedOptions: advancedOptionsState,
evaluationWindow: evaluationWindowState,
notificationSettings: notificationSettingsState,
basicAlertState,
thresholdState,
advancedOptionsState,
evaluationWindowState,
notificationSettingsState,
};
}

View File

@@ -31,7 +31,7 @@ export const listViewInitialLogQuery: Query = {
},
};
export const listViewInitialTraceQuery: Query = {
export const listViewInitialTraceQuery = {
// it should be the above commented query
...initialQueriesMap.traces,
builder: {

View File

@@ -22,7 +22,7 @@ import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import { PenLine, Trash2 } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariablesStore';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { TVariableMode } from './types';

View File

@@ -1,11 +1,8 @@
import { memo, useEffect } from 'react';
import { memo, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { Row } from 'antd';
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
import {
useDashboardVariables,
useDashboardVariablesSelector,
} from 'hooks/dashboard/useDashboardVariables';
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { isEmpty } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard';
@@ -15,7 +12,13 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import DynamicVariableSelection from './DynamicVariableSelection';
import { onUpdateVariableNode } from './util';
import {
buildDependencies,
buildDependencyGraph,
buildParentDependencyGraph,
IDependencyData,
onUpdateVariableNode,
} from './util';
import VariableItem from './VariableItem';
import './DashboardVariableSelection.styles.scss';
@@ -32,11 +35,11 @@ function DashboardVariableSelection(): JSX.Element | null {
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
const { dashboardVariables } = useDashboardVariables();
const sortedVariablesArray = useDashboardVariablesSelector(
(state) => state.sortedVariablesArray,
);
const dependencyData = useDashboardVariablesSelector(
(state) => state.dependencyData,
const [variablesTableData, setVariablesTableData] = useState<any>([]);
const [dependencyData, setDependencyData] = useState<IDependencyData | null>(
null,
);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
@@ -44,6 +47,24 @@ function DashboardVariableSelection(): JSX.Element | null {
);
useEffect(() => {
const tableRowData = [];
// eslint-disable-next-line no-restricted-syntax
for (const [key, value] of Object.entries(dashboardVariables)) {
const { id } = value;
tableRowData.push({
key,
name: key,
...dashboardVariables[key],
id,
});
}
tableRowData.sort((a, b) => a.order - b.order);
setVariablesTableData(tableRowData);
// Initialize variables with default values if not in URL
initializeDefaultVariables(
dashboardVariables,
@@ -52,6 +73,30 @@ function DashboardVariableSelection(): JSX.Element | null {
);
}, [getUrlVariables, updateUrlVariable, dashboardVariables]);
useEffect(() => {
if (variablesTableData.length > 0) {
const depGrp = buildDependencies(variablesTableData);
const { order, graph, hasCycle, cycleNodes } = buildDependencyGraph(depGrp);
const parentDependencyGraph = buildParentDependencyGraph(graph);
// cleanup order to only include variables that are of type 'QUERY'
const cleanedOrder = order.filter((variable) => {
const variableData = variablesTableData.find(
(v: IDashboardVariable) => v.name === variable,
);
return variableData?.type === 'QUERY';
});
setDependencyData({
order: cleanedOrder,
graph,
parentDependencyGraph,
hasCycle,
cycleNodes,
});
}
}, [dashboardVariables, variablesTableData]);
// this handles the case where the dependency order changes i.e. variable list updated via creation or deletion etc. and we need to refetch the variables
// also trigger when the global time changes
useEffect(
@@ -141,30 +186,45 @@ function DashboardVariableSelection(): JSX.Element | null {
}
};
if (!dashboardVariables) {
return null;
}
const orderBasedSortedVariables = variablesTableData.sort(
(a: { order: number }, b: { order: number }) => a.order - b.order,
);
return (
<Row style={{ display: 'flex', gap: '12px' }}>
{sortedVariablesArray.map((variable) => {
const key = `${variable.name}${variable.id}${variable.order}`;
return variable.type === 'DYNAMIC' ? (
<DynamicVariableSelection
key={key}
existingVariables={dashboardVariables}
variableData={variable}
onValueUpdate={onValueUpdate}
/>
) : (
<VariableItem
key={key}
existingVariables={dashboardVariables}
variableData={variable}
onValueUpdate={onValueUpdate}
variablesToGetUpdated={variablesToGetUpdated}
setVariablesToGetUpdated={setVariablesToGetUpdated}
dependencyData={dependencyData}
/>
);
})}
{orderBasedSortedVariables &&
Array.isArray(orderBasedSortedVariables) &&
orderBasedSortedVariables.length > 0 &&
orderBasedSortedVariables.map((variable) =>
variable.type === 'DYNAMIC' ? (
<DynamicVariableSelection
key={`${variable.name}${variable.id}${variable.order}`}
existingVariables={dashboardVariables}
variableData={{
name: variable.name,
...variable,
}}
onValueUpdate={onValueUpdate}
/>
) : (
<VariableItem
key={`${variable.name}${variable.id}}${variable.order}`}
existingVariables={dashboardVariables}
variableData={{
name: variable.name,
...variable,
}}
onValueUpdate={onValueUpdate}
variablesToGetUpdated={variablesToGetUpdated}
setVariablesToGetUpdated={setVariablesToGetUpdated}
dependencyData={dependencyData}
/>
),
)}
</Row>
);
}

View File

@@ -16,7 +16,6 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
import { debounce, isArray, isEmpty, isString } from 'lodash-es';
import { IDependencyData } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { AppState } from 'store/reducers';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
@@ -25,7 +24,7 @@ import { popupContainer } from 'utils/selectPopupContainer';
import { ALL_SELECT_VALUE, variablePropsToPayloadVariables } from '../utils';
import { SelectItemStyle } from './styles';
import { areArraysEqual, checkAPIInvocation } from './util';
import { areArraysEqual, checkAPIInvocation, IDependencyData } from './util';
import './DashboardVariableSelection.styles.scss';

View File

@@ -3,7 +3,7 @@ import { useCallback } from 'react';
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariablesStore';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { v4 as uuidv4 } from 'uuid';

View File

@@ -1,9 +1,6 @@
import { OptionData } from 'components/NewSelect/types';
import { isEmpty, isNull } from 'lodash-es';
import {
IDashboardVariables,
IDependencyData,
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariablesStore';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
export function areArraysEqual(
@@ -100,6 +97,14 @@ export const buildDependencies = (
return graph;
};
export interface IDependencyData {
order: string[];
graph: VariableGraph;
parentDependencyGraph: VariableGraph;
hasCycle: boolean;
cycleNodes?: string[];
}
export const buildParentDependencyGraph = (
graph: VariableGraph,
): VariableGraph => {

View File

@@ -1,104 +0,0 @@
import { useCallback, useRef } from 'react';
import ChartLayout from 'container/DashboardContainer/visualization/layout/ChartLayout/ChartLayout';
import Legend from 'lib/uPlotV2/components/Legend/Legend';
import Tooltip from 'lib/uPlotV2/components/Tooltip/Tooltip';
import {
LegendPosition,
TooltipRenderArgs,
} from 'lib/uPlotV2/components/types';
import UPlotChart from 'lib/uPlotV2/components/UPlotChart';
import { PlotContextProvider } from 'lib/uPlotV2/context/PlotContext';
import TooltipPlugin from 'lib/uPlotV2/plugins/TooltipPlugin/TooltipPlugin';
import _noop from 'lodash-es/noop';
import uPlot from 'uplot';
import { ChartProps } from '../types';
const TOOLTIP_WIDTH_PADDING = 60;
const TOOLTIP_MIN_WIDTH = 200;
export default function TimeSeries({
legendConfig = { position: LegendPosition.BOTTOM },
config,
data,
width: containerWidth,
height: containerHeight,
disableTooltip = false,
canPinTooltip = false,
timezone,
yAxisUnit,
decimalPrecision,
syncMode,
syncKey,
onDestroy = _noop,
children,
layoutChildren,
'data-testid': testId,
}: ChartProps): JSX.Element {
const plotInstanceRef = useRef<uPlot | null>(null);
const legendComponent = useCallback(
(averageLegendWidth: number): React.ReactNode => {
return (
<Legend
config={config}
position={legendConfig.position}
averageLegendWidth={averageLegendWidth}
/>
);
},
[config, legendConfig.position],
);
return (
<PlotContextProvider>
<ChartLayout
config={config}
containerWidth={containerWidth}
containerHeight={containerHeight}
legendConfig={legendConfig}
legendComponent={legendComponent}
layoutChildren={layoutChildren}
>
{({ chartWidth, chartHeight, averageLegendWidth }): JSX.Element => (
<UPlotChart
config={config}
data={data}
width={chartWidth}
height={chartHeight}
plotRef={(plot): void => {
plotInstanceRef.current = plot;
}}
onDestroy={(plot: uPlot): void => {
plotInstanceRef.current = null;
onDestroy(plot);
}}
data-testid={testId}
>
{children}
{!disableTooltip && (
<TooltipPlugin
config={config}
canPinTooltip={canPinTooltip}
syncMode={syncMode}
maxWidth={Math.max(
TOOLTIP_MIN_WIDTH,
averageLegendWidth + TOOLTIP_WIDTH_PADDING,
)}
syncKey={syncKey}
render={(props: TooltipRenderArgs): React.ReactNode => (
<Tooltip
{...props}
timezone={timezone}
yAxisUnit={yAxisUnit}
decimalPrecision={decimalPrecision}
/>
)}
/>
)}
</UPlotChart>
)}
</ChartLayout>
</PlotContextProvider>
);
}

View File

@@ -1,29 +0,0 @@
import { PrecisionOption } from 'components/Graph/types';
import { LegendConfig } from 'lib/uPlotV2/components/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { DashboardCursorSync } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
interface BaseChartProps {
width: number;
height: number;
disableTooltip?: boolean;
timezone: string;
syncMode?: DashboardCursorSync;
syncKey?: string;
canPinTooltip?: boolean;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;
}
interface TimeSeriesChartProps extends BaseChartProps {
config: UPlotConfigBuilder;
legendConfig: LegendConfig;
data: uPlot.AlignedData;
plotRef?: (plot: uPlot | null) => void;
onDestroy?: (plot: uPlot) => void;
children?: React.ReactNode;
layoutChildren?: React.ReactNode;
'data-testid'?: string;
}
export type ChartProps = TimeSeriesChartProps;

View File

@@ -1,36 +1,30 @@
import { MAX_LEGEND_WIDTH } from 'lib/uPlotV2/components/Legend/Legend';
import { LegendConfig, LegendPosition } from 'lib/uPlotV2/components/types';
export interface ChartDimensions {
width: number;
height: number;
legendWidth: number;
legendHeight: number;
averageLegendWidth: number;
legendsPerSet: number;
}
const AVG_CHAR_WIDTH = 8;
const LEGEND_WIDTH_PERCENTILE = 0.85;
const DEFAULT_AVG_LABEL_LENGTH = 15;
const BASE_LEGEND_WIDTH = 16;
const LEGEND_GAP = 16;
const LEGEND_PADDING = 12;
const LEGEND_LINE_HEIGHT = 28;
const LEGEND_LINE_HEIGHT = 36;
const MAX_LEGEND_WIDTH = 400;
/**
* Calculates the average width of the legend items based on the labels of the series.
* @param legends - The labels of the series.
* @returns The average width of the legend items.
* Average text width from series labels (for legendsPerSet).
*/
export function calculateAverageLegendWidth(legends: string[]): number {
if (legends.length === 0) {
return DEFAULT_AVG_LABEL_LENGTH * AVG_CHAR_WIDTH;
return DEFAULT_AVG_LABEL_LENGTH;
}
const lengths = legends.map((l) => l.length).sort((a, b) => a - b);
const index = Math.ceil(LEGEND_WIDTH_PERCENTILE * lengths.length) - 1;
const percentileLength = lengths[Math.max(0, index)];
return BASE_LEGEND_WIDTH + percentileLength * AVG_CHAR_WIDTH;
const averageLabelLength =
legends.reduce((sum, l) => sum + l.length, 0) / legends.length;
return averageLabelLength * AVG_CHAR_WIDTH;
}
/**
@@ -71,7 +65,7 @@ export function calculateChartDimensions({
height: 0,
legendWidth: 0,
legendHeight: 0,
averageLegendWidth: 0,
legendsPerSet: 0,
};
}
@@ -92,15 +86,13 @@ export function calculateChartDimensions({
legendWidth: rightLegendWidth,
legendHeight: containerHeight,
// Single vertical list on the right.
averageLegendWidth: rightLegendWidth,
legendsPerSet: 1,
};
}
const legendRowHeight = LEGEND_LINE_HEIGHT + LEGEND_PADDING;
const legendItemWidth = Math.ceil(
Math.min(approxLegendItemWidth, MAX_LEGEND_WIDTH),
);
const legendItemWidth = Math.min(approxLegendItemWidth, 400);
const legendItemsPerRow = Math.max(
1,
Math.floor((containerWidth - LEGEND_PADDING * 2) / legendItemWidth),
@@ -123,11 +115,17 @@ export function calculateChartDimensions({
maxAllowedLegendHeight,
);
// How many legend items per row in the Legend component.
const legendsPerSet = Math.ceil(
(containerWidth + LEGEND_GAP) /
(Math.min(MAX_LEGEND_WIDTH, approxLegendItemWidth) + LEGEND_GAP),
);
return {
width: containerWidth,
height: Math.max(0, containerHeight - bottomLegendHeight),
legendWidth: containerWidth,
legendHeight: bottomLegendHeight,
averageLegendWidth: legendItemWidth,
legendsPerSet,
};
}

View File

@@ -10,6 +10,7 @@
.chart-layout__legend-wrapper {
padding-left: 0 !important;
padding-right: 12px !important;
}
}

View File

@@ -11,7 +11,6 @@ export interface ChartLayoutProps {
children: (props: {
chartWidth: number;
chartHeight: number;
averageLegendWidth: number;
}) => React.ReactNode;
layoutChildren?: React.ReactNode;
containerWidth: number;
@@ -57,7 +56,6 @@ export default function ChartLayout({
{children({
chartWidth: chartDimensions.width,
chartHeight: chartDimensions.height,
averageLegendWidth: chartDimensions.averageLegendWidth,
})}
</div>
<div
@@ -67,7 +65,7 @@ export default function ChartLayout({
width: chartDimensions.legendWidth,
}}
>
{legendComponent(chartDimensions.averageLegendWidth)}
{legendComponent(chartDimensions.legendsPerSet)}
</div>
</div>
{layoutChildren}

View File

@@ -13,16 +13,3 @@ export interface GraphVisibilityState {
name: string;
dataIndex: SeriesVisibilityItem[];
}
/**
* Context in which a panel is rendered. Used to vary behavior (e.g. persistence,
* interactions) per context.
*/
export enum PanelMode {
/** Panel opened in full-screen / standalone view (e.g. from a dashboard widget). */
STANDALONE_VIEW = 'STANDALONE_VIEW',
/** Panel in the widget builder while editing a dashboard. */
DASHBOARD_EDIT = 'DASHBOARD_EDIT',
/** Panel rendered as a widget on a dashboard (read-only view). */
DASHBOARD_VIEW = 'DASHBOARD_VIEW',
}

View File

@@ -2,7 +2,6 @@
import { MutableRefObject } from 'react';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { render, screen, waitFor } from 'tests/test-utils';
@@ -157,7 +156,6 @@ describe('PanelWrapper with DragSelect', () => {
render(
<PanelWrapper
panelMode={PanelMode.STANDALONE_VIEW}
widget={mockWidget}
queryResponse={mockQueryResponse}
onDragSelect={mockOnDragSelect}

View File

@@ -17,7 +17,6 @@ import WarningPopover from 'components/WarningPopover/WarningPopover';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import useDrilldown from 'container/GridCardLayout/GridCard/FullView/useDrilldown';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import {
@@ -367,7 +366,6 @@ function FullView({
/>
)}
<PanelWrapper
panelMode={PanelMode.STANDALONE_VIEW}
queryResponse={response}
widget={widget}
setRequestData={setRequestData}

View File

@@ -14,7 +14,6 @@ import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplor
import { ToggleGraphProps } from 'components/Graph/types';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import useGetResolvedText from 'hooks/dashboard/useGetResolvedText';
@@ -408,7 +407,6 @@ function WidgetGraphComponent({
ref={graphRef}
>
<PanelWrapper
panelMode={PanelMode.DASHBOARD_VIEW}
widget={widget}
queryResponse={queryResponse}
setRequestData={setRequestData}

View File

@@ -4,7 +4,7 @@ import { ToggleGraphProps } from 'components/Graph/types';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariablesStore';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';

View File

@@ -168,7 +168,7 @@
.ant-pagination {
position: fixed;
bottom: 0;
width: calc(100% - 54px);
width: calc(100% - 64px);
background: rgb(18, 19, 23);
padding: 16px;
margin: 0;

View File

@@ -442,7 +442,7 @@
.ant-pagination {
position: fixed;
bottom: 0;
width: calc(100% - 54px);
width: calc(100% - 64px);
background: var(--bg-ink-500);
padding: 16px;
margin: 0;

View File

@@ -58,7 +58,7 @@
overflow-y: auto;
box-sizing: border-box;
height: calc(100% - 54px);
height: calc(100% - 64px);
&::-webkit-scrollbar {
height: 1rem;

View File

@@ -33,7 +33,7 @@
font-weight: 600;
line-height: 1;
letter-spacing: 0;
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #eceef2);
margin: 0 !important;
}
@@ -43,7 +43,7 @@
font-weight: 400;
line-height: 20px;
letter-spacing: -0.065px;
color: var(--l2-foreground);
color: var(--semantic-secondary-foreground, #adb4c2);
max-width: 317px;
margin: 0 !important;
text-align: center;
@@ -51,8 +51,8 @@
.login-form-card {
width: 100%;
background: var(--bg-neutral-dark-950);
border: 1px solid var(--l2-border);
background: var(--semantic-secondary-background, #121317);
border: 1px solid var(--semantic-secondary-border, #23262e);
border-radius: 4px;
padding: 24px;
display: flex;
@@ -174,12 +174,12 @@
&__docs-button {
color: var(--bg-ink-400);
border-color: var(--bg-vanilla-300);
border-color: var(--bg-vanilla-300, #e9e9e9);
background: transparent;
&:hover {
color: var(--bg-ink-100);
border-color: var(--bg-vanilla-400);
border-color: var(--bg-vanilla-400, #d1d5db);
background: transparent;
}
}
@@ -230,8 +230,8 @@
.login-form-input {
height: 32px;
background: var(--l3-background);
border: 1px solid var(--l3-border);
background: var(--levels-l3-background, #23262e);
border: 1px solid var(--levels-l3-border, #2c303a);
border-radius: 2px;
padding: 6px 8px;
font-family: Inter, sans-serif;
@@ -239,18 +239,18 @@
font-weight: 400;
line-height: 1;
letter-spacing: -0.065px;
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #eceef2);
&::placeholder {
color: var(--l3-foreground);
color: var(--levels-l3-foreground, #747b8b);
}
&:hover {
border-color: var(--l3-border);
border-color: var(--levels-l3-border, #2c303a);
}
&:focus {
border-color: var(--primary);
border-color: var(--semantic-primary-background, #4e74f8);
box-shadow: none;
}
@@ -271,8 +271,8 @@
font-weight: 400 !important;
line-height: 1 !important;
letter-spacing: -0.065px !important;
background: var(--l3-background) !important;
border: 1px solid var(--l3-border) !important;
background: var(--levels-l3-background, #23262e) !important;
border: 1px solid var(--levels-l3-border, #2c303a) !important;
display: flex !important;
align-items: center !important;
}
@@ -296,19 +296,19 @@
}
.ant-select-selection-placeholder {
color: var(--l3-foreground) !important;
color: var(--levels-l3-foreground, #747b8b) !important;
}
.ant-select-selection-item {
color: var(--l1-foreground) !important;
color: var(--levels-l1-foreground, #eceef2) !important;
}
&:hover .ant-select-selector {
border-color: var(--l3-border) !important;
border-color: var(--levels-l3-border, #2c303a) !important;
}
&.ant-select-focused .ant-select-selector {
border-color: var(--primary) !important;
border-color: var(--semantic-primary-background, #4e74f8) !important;
box-shadow: none !important;
}
}
@@ -339,26 +339,26 @@
width: 100%;
height: 32px;
padding: 10px 16px;
background: var(--primary);
background: var(--semantic-primary-background, #4e74f8);
border: none;
border-radius: 2px;
font-family: Inter, sans-serif;
font-size: 11px;
font-weight: 500;
line-height: 1;
color: var(--bg-neutral-dark-50);
color: var(--semantic-primary-foreground, #eceef2);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
&:hover:not(:disabled) {
background: var(--primary);
background: var(--semantic-primary-background, #4e74f8);
opacity: 0.9;
}
&:disabled {
background: var(--primary);
background: var(--semantic-primary-background, #4e74f8);
opacity: 0.6;
cursor: not-allowed;
}
@@ -370,12 +370,12 @@
}
.login-form-description {
color: var(--text-neutral-light-200);
color: var(--text-neutral-light-200, #80828d);
}
.login-form-card {
background: var(--bg-base-white);
border-color: var(--bg-vanilla-300);
background: var(--bg-base-white, #ffffff);
border-color: var(--bg-vanilla-300, #e9e9e9);
}
.forgot-password-link {
@@ -383,36 +383,36 @@
}
.login-form-input {
background: var(--bg-vanilla-200);
border-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-200, #f5f5f5);
border-color: var(--bg-vanilla-300, #e9e9e9);
color: var(--text-ink-500);
&::placeholder {
color: var(--text-neutral-light-200);
color: var(--text-neutral-light-200, #80828d);
}
&:focus {
border-color: var(--primary);
border-color: var(--semantic-primary-background, #4e74f8);
}
// Select component light mode styling
&.ant-select {
.ant-select-selector {
background: var(--bg-vanilla-200) !important;
border-color: var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-200, #f5f5f5) !important;
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
color: var(--text-ink-500) !important;
}
.ant-select-selection-placeholder {
color: var(--text-neutral-light-200) !important;
color: var(--text-neutral-light-200, #80828d) !important;
}
&:hover .ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
}
&.ant-select-focused .ant-select-selector {
border-color: var(--primary) !important;
border-color: var(--semantic-primary-background, #4e74f8) !important;
}
}
}

View File

@@ -7,7 +7,7 @@ export const Label = styled.label`
font-weight: 600;
line-height: 1;
letter-spacing: -0.065px;
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #eceef2);
margin-bottom: 12px;
display: block;
@@ -47,29 +47,29 @@ export const FormContainer = styled(Form)`
& .ant-input,
& .ant-input-password,
& .ant-select-selector {
background: var(--l3-background) !important;
border-color: var(--l3-border) !important;
color: var(--l1-foreground) !important;
background: var(--levels-l3-background, #23262e) !important;
border-color: var(--levels-l3-border, #2c303a) !important;
color: var(--levels-l1-foreground, #eceef2) !important;
.lightMode & {
background: var(--bg-vanilla-200) !important;
border-color: var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-200, #f5f5f5) !important;
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
color: var(--text-ink-500) !important;
}
}
& .ant-input::placeholder {
color: var(--l3-foreground) !important;
color: var(--levels-l3-foreground, #747b8b) !important;
.lightMode & {
color: var(--text-neutral-light-200) !important;
color: var(--text-neutral-light-200, #80828d) !important;
}
}
& .ant-input:focus,
& .ant-input-password:focus,
& .ant-select-focused .ant-select-selector {
border-color: var(--primary) !important;
border-color: var(--semantic-primary-background, #4e74f8) !important;
box-shadow: none !important;
}
`;

View File

@@ -194,7 +194,7 @@
.ant-pagination {
position: fixed;
bottom: 0;
width: calc(100% - 54px);
width: calc(100% - 64px);
background: var(--bg-ink-500);
padding: 16px;
margin: 0;

View File

@@ -168,7 +168,6 @@ function QuerySection({
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
queryComponents={queryComponents}
signalSourceChangeEnabled
savePreviousQuery
/>
</div>
),

View File

@@ -13,7 +13,6 @@ import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplor
import { ToggleGraphProps } from 'components/Graph/types';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
@@ -184,7 +183,6 @@ function WidgetGraph({
}}
>
<PanelWrapper
panelMode={PanelMode.DASHBOARD_EDIT}
widget={selectedWidget}
queryResponse={queryResponse}
setRequestData={setRequestData}

View File

@@ -31,7 +31,7 @@
}
.table-header-cell {
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #eceef2);
font-family: Inter;
font-size: 13px;
font-style: normal;
@@ -111,9 +111,9 @@
height: 32px !important;
border-radius: 2px;
background: var(--l3-background);
border: 1px solid var(--l3-border);
color: var(--l1-foreground);
background: var(--levels-l3-background, #23262e);
border: 1px solid var(--levels-l3-border, #2c303a);
color: var(--levels-l1-foreground, #eceef2);
font-family: Inter, sans-serif;
font-size: 13px;
font-weight: 400;
@@ -123,15 +123,15 @@
box-sizing: border-box;
&::placeholder {
color: var(--l3-foreground);
color: var(--levels-l3-foreground, #747b8b);
}
&:hover {
border-color: var(--l3-border);
border-color: var(--levels-l3-border, #2c303a);
}
&:focus {
border-color: var(--bg-robin-500);
border-color: var(--semantic-primary-background, #4e74f8);
box-shadow: none;
}
}
@@ -142,9 +142,9 @@
.ant-select-selector {
height: 32px !important;
border-radius: 2px !important;
background: var(--l3-background) !important;
border: 1px solid var(--l3-border) !important;
color: var(--l1-foreground) !important;
background: var(--levels-l3-background, #23262e) !important;
border: 1px solid var(--levels-l3-border, #2c303a) !important;
color: var(--levels-l1-foreground, #eceef2) !important;
font-family: Inter, sans-serif !important;
font-size: 13px !important;
font-weight: 400 !important;
@@ -154,25 +154,25 @@
box-sizing: border-box !important;
.ant-select-selection-placeholder {
color: var(--l3-foreground) !important;
color: var(--levels-l3-foreground, #747b8b) !important;
}
.ant-select-selection-item {
color: var(--l1-foreground) !important;
color: var(--levels-l1-foreground, #eceef2) !important;
line-height: 30px !important;
}
}
.ant-select-arrow {
color: var(--l3-foreground) !important;
color: var(--levels-l3-foreground, #747b8b) !important;
}
&.ant-select-focused .ant-select-selector {
border-color: var(--bg-robin-500) !important;
border-color: var(--semantic-primary-background, #4e74f8) !important;
}
&:hover .ant-select-selector {
border-color: var(--bg-robin-500) !important;
border-color: var(--semantic-primary-background, #4e74f8) !important;
}
}
@@ -215,7 +215,7 @@
}
&:focus-visible {
outline: 2px solid var(--bg-robin-500);
outline: 2px solid var(--semantic-primary-background, #4e74f8);
outline-offset: 2px;
}
}
@@ -247,9 +247,9 @@
justify-content: center;
gap: 6px;
border-radius: 2px;
border: 1px dashed var(--l2-border) !important;
border: 1px dashed var(--semantic-secondary-border, #23262e) !important;
background: transparent !important;
color: var(--l2-foreground);
color: var(--semantic-secondary-foreground, #adb4c2);
font-family: Inter, sans-serif;
font-size: 11px;
font-weight: 500;
@@ -263,38 +263,38 @@
// Ensure icon is visible
svg,
[class*='icon'] {
color: var(--l2-foreground) !important;
color: var(--semantic-secondary-foreground, #adb4c2) !important;
display: inline-block !important;
opacity: 1 !important;
}
button,
& {
border: 1px dashed var(--l2-border) !important;
border: 1px dashed var(--semantic-secondary-border, #23262e) !important;
background: transparent !important;
}
&:hover {
border-color: var(--bg-robin-500) !important;
border-color: var(--semantic-primary-background, #4e74f8) !important;
border-style: dashed !important;
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #eceef2);
background: rgba(78, 116, 248, 0.1) !important;
svg,
[class*='icon'] {
color: var(--l1-foreground) !important;
color: var(--levels-l1-foreground, #eceef2) !important;
}
button,
& {
border-color: var(--bg-robin-500) !important;
border-color: var(--semantic-primary-background, #4e74f8) !important;
border-style: dashed !important;
background: rgba(78, 116, 248, 0.1) !important;
}
}
&:focus-visible {
outline: 2px solid var(--bg-robin-500);
outline: 2px solid var(--semantic-primary-background, #4e74f8);
outline-offset: 2px;
}
}
@@ -327,7 +327,7 @@
gap: 8px;
.success-message {
color: var(--bg-success-500);
color: var(--bg-success-500, #00b37e);
}
}
@@ -343,7 +343,7 @@
gap: 8px;
.partially-sent-invites-message {
color: var(--bg-warning-500);
color: var(--bg-warning-500, #fbbd23);
font-size: 12px;
font-weight: 400;
@@ -358,110 +358,110 @@
.lightMode {
.invite-team-members-table-header {
.table-header-cell {
color: var(--l3-foreground);
color: var(--semantic-secondary-foreground, #747b8b);
}
}
.team-member-email-input {
background: var(--bg-vanilla-200) !important;
border-color: var(--bg-vanilla-300) !important;
color: var(--text-ink-500) !important;
background: var(--bg-vanilla-200, #f5f5f5) !important;
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
color: var(--text-ink-500, #1a1d26) !important;
input {
background: var(--bg-vanilla-200) !important;
border-color: var(--bg-vanilla-300) !important;
color: var(--text-ink-500) !important;
background: var(--bg-vanilla-200, #f5f5f5) !important;
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
color: var(--text-ink-500, #1a1d26) !important;
&::placeholder {
color: var(--text-neutral-light-200) !important;
color: var(--text-neutral-light-200, #80828d) !important;
}
&:focus {
border-color: var(--bg-robin-500) !important;
border-color: var(--semantic-primary-background, #4e74f8) !important;
}
}
&::placeholder {
color: var(--text-neutral-light-200) !important;
color: var(--text-neutral-light-200, #80828d) !important;
}
&:hover {
border-color: var(--bg-vanilla-300) !important;
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
}
&:focus {
border-color: var(--bg-robin-500) !important;
border-color: var(--semantic-primary-background, #4e74f8) !important;
}
}
.team-member-role-select {
.ant-select-selector {
background: var(--l3-background) !important;
border: 1px solid var(--l3-border) !important;
color: var(--l1-foreground) !important;
background: var(--levels-l3-background, #ffffff) !important;
border: 1px solid var(--levels-l3-border, #e9e9e9) !important;
color: var(--levels-l1-foreground, #1a1d26) !important;
.ant-select-selection-placeholder {
color: var(--l3-foreground) !important;
color: var(--levels-l3-foreground, #747b8b) !important;
}
.ant-select-selection-item {
color: var(--l1-foreground) !important;
color: var(--levels-l1-foreground, #1a1d26) !important;
}
}
.ant-select-arrow {
color: var(--l3-foreground) !important;
color: var(--levels-l3-foreground, #747b8b) !important;
}
&.ant-select-focused .ant-select-selector {
border-color: var(--bg-robin-500) !important;
border-color: var(--semantic-primary-background, #4e74f8) !important;
}
&:hover .ant-select-selector {
border-color: var(--bg-robin-500) !important;
border-color: var(--semantic-primary-background, #4e74f8) !important;
}
}
.remove-team-member-button {
border: none !important;
background: transparent !important;
color: var(--bg-cherry-500) !important;
color: var(--bg-cherry-500, #f56565) !important;
svg {
color: var(--bg-cherry-500) !important;
color: var(--bg-cherry-500, #f56565) !important;
}
&:hover {
background: rgba(245, 101, 101, 0.1) !important;
color: var(--bg-cherry-500) !important;
color: var(--bg-cherry-500, #f56565) !important;
svg {
color: var(--bg-cherry-500) !important;
color: var(--bg-cherry-500, #f56565) !important;
}
}
}
.add-another-member-button {
border: 1px dashed var(--text-vanilla-300) !important;
border: 1px dashed var(--semantic-secondary-border, #e9e9e9) !important;
background: transparent !important;
color: var(--l3-foreground);
color: var(--semantic-secondary-foreground, #747b8b);
svg,
[class*='icon'] {
color: var(--l3-foreground) !important;
color: var(--semantic-secondary-foreground, #747b8b) !important;
display: inline-block !important;
opacity: 1 !important;
}
&:hover {
border-color: var(--bg-robin-500) !important;
border-color: var(--semantic-primary-background, #4e74f8) !important;
border-style: dashed !important;
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #1a1d26);
background: rgba(78, 116, 248, 0.1) !important;
svg,
[class*='icon'] {
color: var(--l1-foreground) !important;
color: var(--levels-l1-foreground, #1a1d26) !important;
}
}
}
@@ -470,7 +470,7 @@
.invite-users-error-message-container,
.invite-users-success-message-container {
.success-message {
color: var(--bg-success-500);
color: var(--bg-success-500, #00b37e);
}
}
@@ -479,7 +479,7 @@
background-color: var(--bg-vanilla-100);
.partially-sent-invites-message {
color: var(--bg-warning-500);
color: var(--bg-warning-500, #fbbd23);
}
}
}

View File

@@ -49,7 +49,7 @@
font-weight: 600;
line-height: 1;
letter-spacing: 0;
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #eceef2);
margin: 0 !important;
text-align: center;
}
@@ -60,7 +60,7 @@
font-weight: 400;
line-height: 20px;
letter-spacing: -0.065px;
color: var(--l2-foreground);
color: var(--semantic-secondary-foreground, #adb4c2);
max-width: 528px;
margin: 0 !important;
text-align: center;
@@ -99,8 +99,8 @@
align-items: stretch;
gap: 24px;
border-radius: 4px;
border: 1px solid var(--l2-border);
background: var(--bg-neutral-dark-950);
border: 1px solid var(--semantic-secondary-border, #23262e);
background: var(--semantic-secondary-background, #121317);
.ant-form-item {
margin-bottom: 0px !important;
@@ -119,9 +119,9 @@
width: 100%;
height: 80px;
resize: none;
border: 1px solid var(--l3-border);
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--levels-l3-border, #2c303a);
background: var(--levels-l3-background, #23262e);
color: var(--levels-l1-foreground, #eceef2);
border-radius: 2px;
font-family: Inter, sans-serif;
font-size: 13px;
@@ -132,13 +132,13 @@
box-sizing: border-box;
&::placeholder {
color: var(--l3-foreground);
color: var(--levels-l3-foreground, #747b8b);
opacity: 1;
}
&:focus-visible {
outline: none;
border-color: var(--bg-robin-500);
border-color: var(--semantic-primary-background, #4e74f8);
}
}
@@ -164,8 +164,8 @@
font-size: 12px;
height: 32px;
background: var(--bg-ink-300);
border: 1px solid var(--bg-slate-400);
background: var(--Ink-300, #16181d);
border: 1px solid var(--Greyscale-Slate-400, #1d212d);
color: var(--bg-vanilla-400);
}
@@ -173,8 +173,8 @@
font-size: 11px;
height: 32px;
min-width: 80px;
background: var(--bg-ink-300);
border: 1px solid var(--bg-slate-400);
background: var(--Ink-300, #16181d);
border: 1px solid var(--Greyscale-Slate-400, #1d212d);
border-left: 0px;
color: var(--bg-vanilla-400);
}
@@ -182,7 +182,7 @@
}
.question-label {
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #eceef2);
font-variant-numeric: slashed-zero;
font-family: Inter;
font-size: 13px;
@@ -208,8 +208,8 @@
gap: 8px;
border-radius: 2px;
background: transparent;
border: 1px solid var(--l2-border);
color: var(--l2-foreground);
border: 1px solid var(--semantic-secondary-border, #23262e);
color: var(--semantic-secondary-foreground, #adb4c2);
font-family: Inter, sans-serif;
font-size: 11px;
font-weight: 500;
@@ -219,7 +219,7 @@
&:hover {
opacity: 0.8;
border-color: var(--bg-robin-500);
border-color: var(--semantic-primary-background, #4e74f8);
}
&:disabled {
@@ -236,9 +236,9 @@
justify-content: center;
gap: 8px;
border-radius: 2px;
background: var(--bg-robin-500);
background: var(--semantic-primary-background, #4e74f8);
border: none;
color: var(--bg-neutral-dark-50);
color: var(--semantic-primary-foreground, #eceef2);
font-family: Inter, sans-serif;
font-size: 11px;
font-weight: 500;
@@ -267,7 +267,7 @@
border-radius: 2px;
background: transparent;
border: none;
color: var(--l2-foreground);
color: var(--semantic-secondary-foreground, #adb4c2);
font-family: Inter, sans-serif;
font-size: 11px;
font-weight: 500;
@@ -300,7 +300,7 @@
margin-top: 12px;
.ant-slider-mark-text {
color: var(--l3-foreground);
color: var(--levels-l3-foreground, #747b8b);
font-variant-numeric: lining-nums tabular-nums stacked-fractions
slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on;
@@ -343,7 +343,7 @@
.question {
font-family: Inter, sans-serif;
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #eceef2);
font-variant-numeric: slashed-zero;
font-size: 13px;
font-style: normal;
@@ -358,7 +358,7 @@
}
.question-slider {
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #eceef2);
font-variant-numeric: slashed-zero;
font-family: Inter;
font-size: 13px;
@@ -381,18 +381,18 @@
line-height: 1;
letter-spacing: -0.065px;
height: 32px;
border: 1px solid var(--l3-border);
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--levels-l3-border, #2c303a);
background: var(--levels-l3-background, #23262e);
color: var(--levels-l1-foreground, #eceef2);
box-sizing: border-box;
&::placeholder {
color: var(--l3-foreground);
color: var(--levels-l3-foreground, #747b8b);
}
&:focus-visible {
outline: none;
border-color: var(--bg-robin-500);
border-color: var(--semantic-primary-background, #4e74f8);
}
}
@@ -499,7 +499,7 @@
width: calc((528px - 12px) / 2);
min-width: 258px;
flex: 0 0 calc((528px - 12px) / 2);
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #eceef2);
font-family: Inter, sans-serif;
font-size: 13px;
font-weight: 400;
@@ -511,12 +511,12 @@
.ant-radio-inner {
width: 16px;
height: 16px;
border-color: var(--l3-border);
border-color: var(--levels-l3-border, #2c303a);
}
&.ant-radio-checked .ant-radio-inner {
border-color: var(--bg-robin-500);
background-color: var(--bg-robin-500);
border-color: var(--semantic-primary-background, #4e74f8);
background-color: var(--semantic-primary-background, #4e74f8);
}
}
}
@@ -541,7 +541,7 @@
width: 100%;
label {
color: var(--l1-foreground) !important;
color: var(--levels-l1-foreground, #eceef2) !important;
}
&.checkbox-item-others {
@@ -553,9 +553,9 @@
padding: 6px 8px;
gap: 4px;
border-radius: 2px;
border: 1px solid var(--l3-border);
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--levels-l3-border, #2c303a);
background: var(--levels-l3-background, #23262e);
color: var(--levels-l1-foreground, #eceef2);
font-family: Inter, sans-serif;
font-size: 13px;
font-style: normal;
@@ -566,18 +566,18 @@
box-sizing: border-box;
&::placeholder {
color: var(--l3-foreground);
color: var(--levels-l3-foreground, #747b8b);
}
&:focus {
border-color: var(--bg-robin-500);
color: var(--l1-foreground);
border-color: var(--semantic-primary-background, #4e74f8);
color: var(--levels-l1-foreground, #eceef2);
}
}
}
.ant-checkbox-wrapper {
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #eceef2);
font-family: Inter, sans-serif;
font-size: 13px;
font-weight: 400;
@@ -592,19 +592,19 @@
.ant-checkbox-inner {
width: 16px;
height: 16px;
border: 1.5px solid var(--l3-background);
border: 1.5px solid var(--levels-l3-background, #23262e);
border-radius: 2px;
background-color: transparent;
}
&.ant-checkbox-checked .ant-checkbox-inner {
background-color: var(--bg-robin-500);
border-color: var(--bg-robin-500);
background-color: var(--semantic-primary-background, #4e74f8);
border-color: var(--semantic-primary-background, #4e74f8);
}
}
span {
color: var(--l1-foreground) !important;
color: var(--levels-l1-foreground, #eceef2) !important;
}
}
}
@@ -646,7 +646,7 @@
.add-another-member-button,
.remove-team-member-button {
color: var(--l2-foreground);
color: var(--semantic-secondary-foreground, #adb4c2);
text-align: center;
font-variant-numeric: slashed-zero;
font-family: Inter;
@@ -735,11 +735,11 @@
}
.onboarding-header-title {
color: var(--l1-foreground) !important;
color: var(--levels-l1-foreground, #1a1d26) !important;
}
.onboarding-header-subtitle {
color: var(--l3-foreground) !important;
color: var(--semantic-secondary-foreground, #747b8b) !important;
}
.questions-form {
@@ -750,8 +750,8 @@
align-items: stretch;
gap: 24px;
border-radius: 4px;
border: 1px solid var(--text-vanilla-300);
background: var(--text-base-white);
border: 1px solid var(--semantic-secondary-border, #e9e9e9);
background: var(--semantic-secondary-background, #ffffff);
.ant-form-item {
margin-bottom: 0px !important;
@@ -767,18 +767,18 @@
}
.discover-signoz-input {
border: 1px solid var(--l3-border);
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--levels-l3-border, #e9e9e9);
background: var(--levels-l3-background, #ffffff);
color: var(--levels-l1-foreground, #1a1d26);
font-weight: 400;
&::placeholder {
color: var(--l3-foreground);
color: var(--levels-l3-foreground, #747b8b);
opacity: 1;
}
&:focus-visible {
border-color: var(--bg-robin-500);
border-color: var(--semantic-primary-background, #4e74f8);
}
}
}
@@ -808,51 +808,51 @@
}
.question {
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #1a1d26);
}
.question-slider {
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #1a1d26);
}
.checkbox-item {
label {
color: var(--l1-foreground) !important;
color: var(--levels-l1-foreground, #1a1d26) !important;
}
.ant-checkbox-wrapper {
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #1a1d26);
.ant-checkbox {
.ant-checkbox-inner {
border-color: var(--l3-background);
border-color: var(--levels-l3-background, #ffffff);
background-color: transparent;
}
&.ant-checkbox-checked .ant-checkbox-inner {
background-color: var(--bg-robin-500);
border-color: var(--bg-robin-500);
background-color: var(--semantic-primary-background, #4e74f8);
border-color: var(--semantic-primary-background, #4e74f8);
}
}
span {
color: var(--l1-foreground) !important;
color: var(--levels-l1-foreground, #1a1d26) !important;
}
}
&.checkbox-item-others {
.onboarding-questionaire-other-input {
border: 1px solid var(--l3-border);
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--levels-l3-border, #e9e9e9);
background: var(--levels-l3-background, #ffffff);
color: var(--levels-l1-foreground, #1a1d26);
&::placeholder {
color: var(--l3-foreground);
color: var(--levels-l3-foreground, #747b8b);
}
&:focus {
border-color: var(--bg-robin-500);
color: var(--l1-foreground);
border-color: var(--semantic-primary-background, #4e74f8);
color: var(--levels-l1-foreground, #1a1d26);
}
}
}
@@ -860,32 +860,32 @@
.observability-tool-others-item {
.onboarding-questionaire-other-input {
border: 1px solid var(--l3-border);
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--levels-l3-border, #e9e9e9);
background: var(--levels-l3-background, #ffffff);
color: var(--levels-l1-foreground, #1a1d26);
&::placeholder {
color: var(--l3-foreground);
color: var(--levels-l3-foreground, #747b8b);
}
&:focus {
border-color: var(--bg-robin-500);
color: var(--l1-foreground);
border-color: var(--semantic-primary-background, #4e74f8);
color: var(--levels-l1-foreground, #1a1d26);
}
}
}
input[type='text'] {
border: 1px solid var(--l3-border);
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--levels-l3-border, #e9e9e9);
background: var(--levels-l3-background, #ffffff);
color: var(--levels-l3-foreground, #1a1d26);
&::placeholder {
color: var(--l3-foreground);
color: var(--levels-l3-foreground, #747b8b);
}
&:focus-visible {
border-color: var(--bg-robin-500);
border-color: var(--semantic-primary-background, #4e74f8);
}
}
@@ -981,16 +981,16 @@
.opentelemetry-radio-group {
.opentelemetry-radio-items-wrapper {
.opentelemetry-radio-item {
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #1a1d26);
.ant-radio {
.ant-radio-inner {
border-color: var(--l3-border);
border-color: var(--levels-l3-border, #e9e9e9);
}
&.ant-radio-checked .ant-radio-inner {
border-color: var(--bg-robin-500);
background-color: var(--bg-robin-500);
border-color: var(--semantic-primary-background, #4e74f8);
background-color: var(--semantic-primary-background, #4e74f8);
}
}
}
@@ -999,11 +999,11 @@
}
.onboarding-back-button {
border-color: var(--text-vanilla-300);
color: var(--l3-foreground);
border-color: var(--semantic-secondary-border, #e9e9e9);
color: var(--semantic-secondary-foreground, #747b8b);
&:hover {
border-color: var(--bg-robin-500);
border-color: var(--semantic-primary-background, #4e74f8);
}
}
}

View File

@@ -21,7 +21,6 @@ function PanelWrapper({
onOpenTraceBtnClick,
customSeries,
customOnRowClick,
panelMode,
enableDrillDown = false,
}: PanelWrapperProps): JSX.Element {
const Component = PanelTypeVsPanelWrapper[
@@ -34,7 +33,6 @@ function PanelWrapper({
}
return (
<Component
panelMode={panelMode}
widget={widget}
queryResponse={queryResponse}
setRequestData={setRequestData}

View File

@@ -1,4 +1,3 @@
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { render } from 'tests/test-utils';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -12,7 +11,6 @@ describe('Table panel wrappper tests', () => {
it('table should render fine with the query response and column units', () => {
const { container, getByText } = render(
<TablePanelWrapper
panelMode={PanelMode.DASHBOARD_VIEW}
widget={(tablePanelWidgetQuery as unknown) as Widgets}
queryResponse={(tablePanelQueryResponse as unknown) as any}
onDragSelect={(): void => {}}

View File

@@ -1,4 +1,3 @@
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { render } from 'tests/test-utils';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -21,7 +20,6 @@ describe('Value panel wrappper tests', () => {
it('should render value panel correctly with yaxis unit', () => {
const { getByText } = render(
<ValuePanelWrapper
panelMode={PanelMode.DASHBOARD_VIEW}
widget={(valuePanelWidget as unknown) as Widgets}
queryResponse={(valuePanelQueryResponse as unknown) as any}
onDragSelect={(): void => {}}
@@ -36,7 +34,6 @@ describe('Value panel wrappper tests', () => {
it('should render tooltip when there are conflicting thresholds', () => {
const { getByTestId, container } = render(
<ValuePanelWrapper
panelMode={PanelMode.DASHBOARD_VIEW}
widget={({ ...valuePanelWidget, thresholds } as unknown) as Widgets}
queryResponse={(valuePanelQueryResponse as unknown) as any}
onDragSelect={(): void => {}}

View File

@@ -1,7 +1,6 @@
import { Dispatch, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { WidgetGraphComponentProps } from 'container/GridCardLayout/GridCard/types';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
@@ -32,7 +31,6 @@ export type PanelWrapperProps = {
customOnRowClick?: (record: RowData) => void;
customSeries?: (data: QueryData[]) => uPlot.Series[];
enableDrillDown?: boolean;
panelMode: PanelMode;
};
export type TooltipData = {

View File

@@ -37,7 +37,6 @@ export type QueryBuilderProps = {
onChangeTraceView?: (view: TraceView) => void;
onSignalSourceChange?: (value: string) => void;
signalSourceChangeEnabled?: boolean;
savePreviousQuery?: boolean;
};
export enum TraceView {

View File

@@ -40,7 +40,6 @@
font-size: 12px;
font-style: normal;
font-weight: 400;
pointer-events: none;
// line-height: 18px;
color: var(--bg-sakura-400) !important;

View File

@@ -96,7 +96,6 @@ export default function QueryFunctions({
const isDarkMode = useIsDarkMode();
const hasAnomalyFunction = functions.some((func) => func.name === 'anomaly');
const hasFunctions = functions.length > 0;
const handleAddNewFunction = (): void => {
const defaultFunctionStruct =
@@ -181,14 +180,10 @@ export default function QueryFunctions({
<div
className={cx(
'query-functions-container',
hasFunctions ? 'hasFunctions' : '',
functions && functions.length > 0 ? 'hasFunctions' : '',
)}
>
<Button
className="periscope-btn function-btn"
disabled={hasFunctions}
onClick={handleAddNewFunction}
>
<Button className="periscope-btn function-btn">
<FunctionIcon
className="function-icon"
fillColor={!isDarkMode ? '#0B0C0E' : 'white'}

View File

@@ -12,7 +12,7 @@ import {
initialQueriesMap,
initialQueryBuilderFormValues,
} from 'constants/queryBuilder';
import { IUseDashboardVariablesReturn } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { IUseDashboardVariablesReturn } from 'hooks/dashboard/useDashboardVariables';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';

View File

@@ -29,7 +29,7 @@
font-weight: 600;
line-height: 1;
letter-spacing: 0;
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #eceef2);
margin: 0 !important;
}
@@ -39,7 +39,7 @@
font-weight: 400;
line-height: 20px;
letter-spacing: -0.065px;
color: var(--l2-foreground);
color: var(--semantic-secondary-foreground, #adb4c2);
margin: 0 !important;
text-align: center;
}
@@ -48,12 +48,12 @@
margin-top: 8px;
padding: 4px 12px;
border-radius: 4px;
background: var(--bg-neutral-dark-950);
border: 1px solid var(--l3-background);
background: var(--semantic-secondary-background, #121317);
border: 1px solid var(--semantic-secondary-border, #23262e);
font-size: 11px;
font-weight: 400;
line-height: 1.45;
color: var(--l2-foreground);
color: var(--semantic-secondary-foreground);
text-align: center;
}
}
@@ -63,8 +63,8 @@
.reset-password-form-container {
width: 100%;
background: var(--bg-neutral-dark-950);
border: 1px solid var(--l3-background);
background: var(--semantic-secondary-background, #121317);
border: 1px solid var(--semantic-secondary-border, #23262e);
border-radius: 4px;
padding: 24px;
@@ -91,8 +91,8 @@
&.ant-input-affix-wrapper {
height: 32px;
border-radius: 2px;
background: var(--l3-background);
border-color: var(--l3-border);
background: var(--levels-l3-background, #23262e);
border-color: var(--levels-l3-border, #2c303a);
}
&.ant-input-affix-wrapper {
@@ -149,27 +149,27 @@
}
.reset-password-header-subtitle {
color: var(--text-neutral-light-200);
color: var(--text-neutral-light-200, #80828d);
}
.reset-password-version-badge {
background: var(--bg-vanilla-200);
border: 1px solid var(--bg-vanilla-300);
color: var(--text-neutral-light-200);
background: var(--bg-vanilla-200, #f5f5f5);
border: 1px solid var(--bg-vanilla-300, #e9e9e9);
color: var(--text-neutral-light-200, #80828d);
}
}
.reset-password-form {
.reset-password-form-container {
background: var(--bg-base-white);
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-base-white, #ffffff);
border: 1px solid var(--bg-vanilla-300, #e9e9e9);
.reset-password-form-input {
&.ant-input,
&.ant-input-password,
&.ant-input-affix-wrapper {
background: var(--bg-vanilla-200);
border-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-200, #f5f5f5);
border-color: var(--bg-vanilla-300, #e9e9e9);
color: var(--text-ink-500);
}
@@ -181,11 +181,11 @@
}
&::placeholder {
color: var(--text-neutral-light-200);
color: var(--text-neutral-light-200, #80828d);
}
&:focus {
border-color: var(--primary);
border-color: var(--semantic-primary-background, #4e74f8);
}
}
}

View File

@@ -13,12 +13,6 @@
.nav-item-active-marker {
background: #4e74f8;
}
.nav-item-data {
.nav-item-label {
color: var(--bg-vanilla-100, #fff);
}
}
}
&.disabled {
@@ -33,14 +27,14 @@
.nav-item-data {
color: white;
background: var(--bg-slate-500, #161922);
background: var(--Slate-500, #161922);
}
}
&.active {
.nav-item-data {
color: white;
background: var(--bg-slate-500, #161922);
background: var(--Slate-500, #161922);
// color: #3f5ecc;
}
}
@@ -56,9 +50,9 @@
.nav-item-data {
flex-grow: 1;
max-width: calc(100% - 20px);
max-width: calc(100% - 24px);
display: flex;
margin: 0px 0px 0px 6px;
margin: 0px 8px;
padding: 2px 8px;
flex-direction: row;
align-items: center;
@@ -74,7 +68,7 @@
background: transparent;
transition: 0.08s all ease;
transition: 0.2s all linear;
border-radius: 3px;
@@ -106,7 +100,7 @@
&:hover {
.nav-item-label {
color: var(--bg-vanilla-100, #fff);
color: var(--Vanilla-100, #fff);
}
.nav-item-pin-icon {
@@ -126,12 +120,6 @@
.nav-item-active-marker {
background: #4e74f8;
}
.nav-item-data {
.nav-item-label {
color: var(--bg-slate-500);
}
}
}
&:hover {

View File

@@ -1,12 +1,11 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import { Tag, Tooltip } from 'antd';
import { Tag } from 'antd';
import cx from 'classnames';
import { Pin, PinOff } from 'lucide-react';
import { SidebarItem } from '../sideNav.types';
import './NavItem.styles.scss';
import './NavItem.styles.scss';
export default function NavItem({
@@ -75,25 +74,21 @@ export default function NavItem({
)}
{onTogglePin && !isPinned && (
<Tooltip title="Add to shortcuts" placement="right">
<Pin
size={12}
className="nav-item-pin-icon"
onClick={handleTogglePinClick}
color="var(--Vanilla-400, #c0c1c3)"
/>
</Tooltip>
<Pin
size={12}
className="nav-item-pin-icon"
onClick={handleTogglePinClick}
color="var(--Vanilla-400, #c0c1c3)"
/>
)}
{onTogglePin && isPinned && (
<Tooltip title="Remove from shortcuts" placement="right">
<PinOff
size={12}
className="nav-item-pin-icon"
onClick={handleTogglePinClick}
color="var(--Vanilla-400, #c0c1c3)"
/>
</Tooltip>
<PinOff
size={12}
className="nav-item-pin-icon"
onClick={handleTogglePinClick}
color="var(--Vanilla-400, #c0c1c3)"
/>
)}
</div>
</div>

View File

@@ -1,5 +1,5 @@
.sidenav-container {
width: 54px;
width: 64px;
height: 100%;
position: relative;
z-index: 1;
@@ -10,60 +10,47 @@
}
.sideNav {
flex: 0 0 54px;
flex: 0 0 64px;
height: 100%;
max-width: 54px;
min-width: 54px;
width: 54px;
border-right: 1px solid var(--bg-slate-500, #161922);
background: var(--bg-ink-500, #0b0c0e);
max-width: 64px;
min-width: 64px;
width: 64px;
border-right: 1px solid var(--Slate-500, #161922);
background: var(--Ink-500, #0b0c0e);
padding-bottom: 48px;
transition: all 0.08s ease, background 0s, border 0s;
transition: all 0.2s, background 0s, border 0s;
.brand-container {
padding: 8px 15px;
padding: 8px 18px;
max-width: 100%;
background: transparent;
}
.brand-company-meta {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
width: 100%;
justify-content: center;
gap: 8px;
}
.brand {
display: flex;
align-items: center;
justify-content: center;
max-width: 100%;
overflow: hidden;
gap: 32px;
height: 32px;
width: 100%;
.brand-logo {
display: flex;
align-items: center;
justify-content: center;
justify-content: space-between;
gap: 8px;
flex-shrink: 0;
width: 20px;
height: 16px;
position: relative;
cursor: pointer;
img {
height: 16px;
width: auto;
display: block;
}
.brand-logo-name {
@@ -79,10 +66,6 @@
.brand-title-section {
display: none;
flex-shrink: 0;
align-items: center;
gap: 0;
position: relative;
.license-type {
display: flex;
@@ -93,7 +76,7 @@
color: var(--bg-vanilla-100);
border-radius: 4px 0px 0px 4px;
background: var(--bg-slate-400, #1d212d);
background: var(--Slate-400, #1d212d);
text-align: center;
font-family: Inter;
@@ -115,11 +98,11 @@
gap: 6px;
border-radius: 0px 4px 4px 0px;
background: var(--bg-slate-300, #242834);
background: var(--Slate-300, #242834);
}
.version {
color: var(--bg-vanilla-400, #c0c1c3);
color: var(--Vanilla-400, #c0c1c3);
text-align: center;
font-variant-numeric: lining-nums tabular-nums slashed-zero;
font-feature-settings: 'salt' on;
@@ -173,48 +156,24 @@
.get-started-nav-items {
display: flex;
margin: 4px 10px 12px 8px;
margin: 4px 13px 12px 10px;
.get-started-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
margin-left: 2px;
gap: 8px;
width: 100%;
height: 32px;
border-radius: 3px;
border: 1px solid var(--bg-slate-400, #1d212d);
background: var(--bg-slate-500, #161922);
border: 1px solid var(--Slate-400, #1d212d);
background: var(--Slate-500, #161922);
box-shadow: none !important;
color: var(--bg-vanilla-400, #c0c1c3);
svg {
color: var(--bg-vanilla-400, #c0c1c3);
}
.nav-item-label {
color: var(--bg-vanilla-400, #c0c1c3);
}
&:hover:not(:disabled) {
background: var(--bg-slate-400, #1d212d);
border-color: var(--bg-slate-400, #1d212d);
color: var(--bg-vanilla-100, #fff);
svg {
color: var(--bg-vanilla-100, #fff);
}
.nav-item-label {
color: var(--bg-vanilla-100, #fff);
}
}
}
}
@@ -233,10 +192,6 @@
width: 100%;
}
.nav-item {
margin-bottom: 6px;
}
.nav-top-section {
display: flex;
flex-direction: column;
@@ -272,7 +227,7 @@
}
.nav-section-title {
color: var(--bg-slate-50, #62687c);
color: var(--Slate-50, #62687c);
font-family: Inter;
font-size: 11px;
font-style: normal;
@@ -286,7 +241,7 @@
align-items: center;
gap: 8px;
padding: 0 17px;
padding: 0 20px;
.nav-section-title-text {
display: none;
@@ -295,17 +250,11 @@
.nav-section-title-icon {
display: flex;
align-items: center;
transition: opacity 0.08s ease, transform 0.08s ease;
&.reorder {
display: none;
cursor: pointer;
margin-left: auto;
transition: color 0.2s;
&:hover {
color: var(--bg-vanilla-100, #fff);
}
}
}
@@ -319,7 +268,7 @@
}
.nav-section-subtitle {
color: var(--bg-vanilla-400, #c0c1c3);
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 11px;
font-style: normal;
@@ -327,20 +276,20 @@
line-height: 14px; /* 150% */
letter-spacing: 0.4px;
padding: 6px 20px;
padding: 0 20px;
opacity: 0.6;
display: none;
transition: all 0.08s ease, background 0s, border 0s;
transition-delay: 0.03s;
transition: all 0.3s, background 0s, border 0s;
transition-delay: 0.1s;
}
.nav-items-section {
margin-top: 8px;
display: flex;
flex-direction: column;
transition: all 0.08s ease;
gap: 4px;
}
}
@@ -353,7 +302,7 @@
.nav-items-section {
opacity: 0;
transform: translateY(-10px);
transition: all 0.1s ease;
transition: all 0.4s ease;
overflow: hidden;
height: 0;
}
@@ -363,34 +312,11 @@
.nav-items-section {
opacity: 1;
transform: translateY(0);
transition: all 0.1s ease;
transition: all 0.4s ease;
overflow: hidden;
height: auto;
}
}
&.sidebar-collapsed {
.nav-title-section {
display: none;
}
.nav-items-section {
margin-top: 0;
opacity: 1;
transform: translateY(0);
transition: all 0.08s ease;
height: auto;
overflow: visible;
}
}
}
.shortcut-nav-items {
&.sidebar-collapsed {
.nav-items-section {
margin-top: 0;
}
}
}
.scroll-for-more-container {
@@ -400,7 +326,7 @@
width: 100%;
bottom: 12px;
bottom: 8px;
margin-left: 43px;
margin-left: 50px;
.scroll-for-more {
display: flex;
@@ -444,6 +370,8 @@
overflow-y: auto;
overflow-x: hidden;
padding-top: 12px;
.secondary-nav-items {
display: flex;
flex-direction: column;
@@ -453,10 +381,10 @@
overflow-x: hidden;
padding: 8px 0;
max-width: 100%;
width: 54px;
width: 64px;
// width: 100%; // temp
transition: all 0.08s ease, background 0s, border 0s;
transition: all 0.2s, background 0s, border 0s;
background: linear-gradient(180deg, rgba(11, 12, 14, 0) 0%, #0b0c0e 27.11%);
@@ -485,7 +413,7 @@
&.scroll-available {
.nav-bottom-section {
border-top: 1px solid var(--bg-slate-500, #161922);
border-top: 1px solid var(--Slate-500, #161922);
}
}
}
@@ -496,53 +424,24 @@
}
&.collapsed {
flex: 0 0 54px;
max-width: 54px;
min-width: 54px;
width: 54px;
flex: 0 0 64px;
max-width: 64px;
min-width: 64px;
width: 64px;
.nav-wrapper {
.nav-top-section {
.shortcut-nav-items {
.nav-section-title {
display: none;
}
.nav-section-title,
.nav-section-subtitle {
display: none;
}
.nav-items-section {
display: flex;
margin-top: 0;
}
.nav-title-section {
margin-top: 0;
margin-bottom: 0;
gap: 0;
}
}
.more-nav-items {
.nav-section-title {
display: none;
}
.nav-items-section {
display: flex;
margin-top: 0;
}
.nav-title-section {
display: none;
}
}
}
.nav-bottom-section {
.secondary-nav-items {
width: 54px;
width: 64px;
}
}
}
@@ -567,7 +466,7 @@
border-radius: 12px;
background: var(--Robin-500, #4e74f8);
color: var(--bg-vanilla-100, #fff);
color: var(--Vanilla-100, #fff);
font-variant-numeric: lining-nums tabular-nums slashed-zero;
font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on;
font-family: Inter;
@@ -580,7 +479,7 @@
}
.sidenav-beta-tag {
color: var(--bg-vanilla-100, #fff);
color: var(--Vanilla-100, #fff);
font-variant-numeric: lining-nums tabular-nums slashed-zero;
font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on;
font-family: Inter;
@@ -595,47 +494,7 @@
background: var(--bg-slate-300);
}
&:not(.pinned) {
.nav-item {
.nav-item-data {
justify-content: center;
}
}
.shortcut-nav-items,
.more-nav-items {
.nav-section-title {
padding: 0 17px;
.nav-section-title-icon {
display: none;
}
}
}
&.dropdown-open {
.nav-item {
.nav-item-data {
flex-grow: 1;
justify-content: flex-start;
}
}
.shortcut-nav-items,
.more-nav-items {
.nav-section-title {
padding: 0 17px;
.nav-section-title-icon {
display: flex;
}
}
}
}
}
&:not(.pinned):hover,
&.dropdown-open {
&:hover {
flex: 0 0 240px;
max-width: 240px;
min-width: 240px;
@@ -646,17 +505,8 @@
z-index: 10;
background: #0b0c0e;
.brand-container {
padding: 8px 15px;
}
.brand {
justify-content: flex-start;
.brand-company-meta {
justify-content: flex-start;
width: 100%;
}
justify-content: space-between;
.brand-title-section {
display: flex;
@@ -683,11 +533,6 @@
.nav-section-title-icon {
&.reorder {
display: flex;
transition: color 0.2s;
&:hover {
color: var(--bg-vanilla-100, #fff);
}
}
}
}
@@ -729,7 +574,7 @@
flex-direction: row;
gap: 3px;
border-radius: 20px;
background: var(--bg-slate-400, #1d212d);
background: var(--Slate-400, #1d212d);
/* Drop Shadow */
box-shadow: 0px 103px 12px 0px rgba(0, 0, 0, 0.01),
@@ -745,7 +590,7 @@
width: 140px;
.scroll-for-more-label {
color: var(--bg-vanilla-400, #c0c1c3);
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
@@ -786,13 +631,6 @@
align-items: flex-start;
}
}
.nav-item {
.nav-item-data {
flex-grow: 1;
justify-content: flex-start;
}
}
}
.get-started-nav-items {
@@ -826,17 +664,8 @@
z-index: 10;
background: #0b0c0e;
.brand-container {
padding: 8px 15px;
}
.brand {
justify-content: flex-start;
.brand-company-meta {
justify-content: flex-start;
width: 100%;
}
justify-content: space-between;
.brand-title-section {
display: flex;
@@ -863,11 +692,6 @@
.nav-section-title-icon {
&.reorder {
display: flex;
transition: color 0.2s;
&:hover {
color: var(--bg-vanilla-100, #fff);
}
}
}
}
@@ -909,7 +733,7 @@
flex-direction: row;
gap: 3px;
border-radius: 20px;
background: var(--bg-slate-400, #1d212d);
background: var(--Slate-400, #1d212d);
/* Drop Shadow */
box-shadow: 0px 103px 12px 0px rgba(0, 0, 0, 0.01),
@@ -927,7 +751,7 @@
.scroll-for-more-label {
display: block;
color: var(--bg-vanilla-400, #c0c1c3);
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
@@ -1032,7 +856,7 @@
.ant-dropdown-menu-item {
.ant-dropdown-menu-title-content {
color: var(--bg-vanilla-400, #c0c1c3);
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
@@ -1040,12 +864,6 @@
line-height: normal;
letter-spacing: 0.14px;
}
&:hover:not(.ant-dropdown-menu-item-disabled) {
.ant-dropdown-menu-title-content {
color: var(--bg-vanilla-100, #fff);
}
}
}
}
}
@@ -1057,7 +875,7 @@
gap: 8px;
.user-settings-dropdown-label-text {
color: var(--bg-slate-50, #62687c);
color: var(--Slate-50, #62687c);
font-family: Inter;
font-size: 10px;
font-family: Inter;
@@ -1069,7 +887,7 @@
}
.user-settings-dropdown-label-email {
color: var(--bg-vanilla-400, #c0c1c3);
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
@@ -1079,16 +897,12 @@
}
.ant-dropdown-menu-item-divider {
background-color: var(--bg-slate-500, #161922) !important;
background-color: var(--Slate-500, #161922) !important;
}
.ant-dropdown-menu-item-disabled {
opacity: 0.7;
}
.ant-dropdown-menu {
width: 100% !important;
}
}
.settings-dropdown,
@@ -1098,27 +912,6 @@
}
}
.secondary-nav-items {
.nav-item {
position: relative;
.nav-item-active-marker {
position: absolute;
left: -5px;
top: 50%;
transform: translateY(-50%);
margin: 0;
width: 8px;
height: 24px;
z-index: 1;
}
}
.nav-item-data {
margin-left: 8px !important;
}
}
.reorder-shortcut-nav-items-modal {
width: 384px !important;
@@ -1235,6 +1028,7 @@
display: flex;
align-items: center;
border-radius: 2px;
border-radius: 2px;
background: var(--Robin-500, #4e74f8) !important;
color: var(--bg-vanilla-100) !important;
font-family: Inter;
@@ -1244,10 +1038,10 @@
line-height: 24px;
&.secondary-btn {
background-color: var(--bg-slate-500, #161922) !important;
background-color: var(--Slate-500, #161922) !important;
border: 1px solid var(--bg-slate-500) !important;
color: var(--bg-vanilla-400, #c0c1c3) !important;
color: var(--Vanilla-400, #c0c1c3) !important;
/* button/ small */
font-family: Inter;
@@ -1270,10 +1064,6 @@
}
}
.help-support-dropdown li.ant-dropdown-menu-item-divider {
background-color: var(--bg-slate-500, #161922) !important;
}
.lightMode {
.sideNav {
background: var(--bg-vanilla-100);
@@ -1305,32 +1095,8 @@
.get-started-nav-items {
.get-started-btn {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-200);
color: var(--bg-slate-50, #62687c);
svg {
color: var(--bg-slate-50, #62687c);
}
.nav-item-label {
color: var(--bg-ink-400, #62687c);
}
// Hover state (light mode)
&:hover:not(:disabled) {
background: var(--bg-vanilla-300);
border-color: var(--bg-vanilla-300);
color: var(--bg-slate-500, #161922);
svg {
color: var(--bg-slate-500, #161922);
}
.nav-item-label {
color: var(--bg-slate-500, #161922);
}
}
background: var(--bg-vanilla-100);
color: var(--bg-ink-400);
}
}
@@ -1342,25 +1108,7 @@
}
}
.brand-container {
background: transparent;
}
.nav-wrapper {
.nav-top-section {
.shortcut-nav-items {
.nav-section-title {
.nav-section-title-icon {
&.reorder {
&:hover {
color: var(--bg-slate-400, #1d212d);
}
}
}
}
}
}
.secondary-nav-items {
border-top: 1px solid var(--bg-vanilla-300);
@@ -1375,43 +1123,8 @@
}
}
&.pinned {
.nav-wrapper {
.nav-top-section {
.shortcut-nav-items {
.nav-section-title {
.nav-section-title-icon {
&.reorder {
&:hover {
color: var(--bg-slate-400, #1d212d);
}
}
}
}
}
}
}
}
&:not(.pinned):hover,
&.dropdown-open {
&:hover {
background: var(--bg-vanilla-100);
.nav-wrapper {
.nav-top-section {
.shortcut-nav-items {
.nav-section-title {
.nav-section-title-icon {
&.reorder {
&:hover {
color: var(--bg-slate-400, #1d212d);
}
}
}
}
}
}
}
}
}
@@ -1421,12 +1134,6 @@
.ant-dropdown-menu-title-content {
color: var(--bg-ink-400);
}
&:hover:not(.ant-dropdown-menu-item-disabled) {
.ant-dropdown-menu-title-content {
color: var(--bg-ink-500);
}
}
}
}
}
@@ -1503,10 +1210,6 @@
color: var(--bg-ink-400);
}
}
.help-support-dropdown li.ant-dropdown-menu-item-divider {
background-color: var(--bg-vanilla-300) !important;
}
}
.version-tooltip-overlay {
@@ -1519,7 +1222,7 @@
border-radius: 2px;
border: 1px solid var(--bg-slate-500);
color: var(--bg-vanilla-100, #fff);
color: var(--Vanilla-100, #fff);
font-family: Inter;
font-size: 11px;
font-style: normal;
@@ -1534,7 +1237,7 @@
gap: 4px;
.version-update-notification-tooltip-title {
color: var(--bg-vanilla-100, #fff);
color: var(--Vanilla-100, #fff);
font-family: Inter;
font-size: 11px;
font-style: normal;
@@ -1544,7 +1247,7 @@
}
.version-update-notification-tooltip-content {
color: var(--bg-vanilla-100, #fff);
color: var(--Vanilla-100, #fff);
font-family: Inter;
font-size: 10px;
font-style: normal;

View File

@@ -157,27 +157,18 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
DefaultHelpSupportDropdownMenuItems,
);
const [pinnedMenuItems, setPinnedMenuItems] = useState<SidebarItem[]>([]);
const [tempPinnedMenuItems, setTempPinnedMenuItems] = useState<SidebarItem[]>(
[],
);
const [secondaryMenuItems, setSecondaryMenuItems] = useState<SidebarItem[]>(
[],
);
const [hasScroll, setHasScroll] = useState(false);
const navTopSectionRef = useRef<HTMLDivElement>(null);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const [pinnedMenuItems, setPinnedMenuItems] = useState<SidebarItem[]>([]);
const [secondaryMenuItems, setSecondaryMenuItems] = useState<SidebarItem[]>(
[],
);
const handleMouseEnter = useCallback(() => {
setIsHovered(true);
}, []);
const handleMouseLeave = useCallback(() => {
setIsHovered(false);
}, []);
const checkScroll = useCallback((): void => {
if (navTopSectionRef.current) {
@@ -226,68 +217,63 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const isAdmin = user.role === USER_ROLES.ADMIN;
const isEditor = user.role === USER_ROLES.EDITOR;
// Compute initial pinned items and secondary menu items synchronously to avoid flash
const computedPinnedMenuItems = useMemo(() => {
const navShortcutsPreference = userPreferences?.find(
useEffect(() => {
const navShortcuts = (userPreferences?.find(
(preference) => preference.name === USER_PREFERENCES.NAV_SHORTCUTS,
);
const navShortcuts = (navShortcutsPreference?.value as unknown) as
| string[]
| undefined;
)?.value as unknown) as string[];
// If userPreferences not loaded yet, return empty to avoid showing defaults before preferences load
if (userPreferences === null) {
return [];
}
const shouldShowIntegrations =
(isCloudUser || isEnterpriseSelfHostedUser) && (isAdmin || isEditor);
// If preference exists with non-empty array, use stored shortcuts
if (isArray(navShortcuts) && navShortcuts.length > 0) {
return navShortcuts
if (navShortcuts && isArray(navShortcuts) && navShortcuts.length > 0) {
// nav shortcuts is array of strings
const pinnedItems = navShortcuts
.map((shortcut) =>
defaultMoreMenuItems.find((item) => item.itemKey === shortcut),
)
.filter((item): item is SidebarItem => item !== undefined);
// Set pinned items in the order they were stored
setPinnedMenuItems(pinnedItems);
setSecondaryMenuItems(
defaultMoreMenuItems.map((item) => ({
...item,
isPinned: pinnedItems.some((pinned) => pinned.itemKey === item.itemKey),
isEnabled:
item.key === ROUTES.INTEGRATIONS
? shouldShowIntegrations
: item.isEnabled,
})),
);
} else {
// Set default pinned items
const defaultPinnedItems = defaultMoreMenuItems.filter(
(item) => item.isPinned,
);
setPinnedMenuItems(defaultPinnedItems);
setSecondaryMenuItems(
defaultMoreMenuItems.map((item) => ({
...item,
isPinned: defaultPinnedItems.some(
(pinned) => pinned.itemKey === item.itemKey,
),
isEnabled:
item.key === ROUTES.INTEGRATIONS
? shouldShowIntegrations
: item.isEnabled,
})),
);
}
// No preference, or empty array → use defaults
return defaultMoreMenuItems.filter((item) => item.isPinned);
}, [userPreferences]);
const computedSecondaryMenuItems = useMemo(() => {
const shouldShowIntegrationsValue =
(isCloudUser || isEnterpriseSelfHostedUser) && (isAdmin || isEditor);
return defaultMoreMenuItems.map((item) => ({
...item,
isPinned: computedPinnedMenuItems.some(
(pinned) => pinned.itemKey === item.itemKey,
),
isEnabled:
item.key === ROUTES.INTEGRATIONS
? shouldShowIntegrationsValue
: item.isEnabled,
}));
}, [
computedPinnedMenuItems,
userPreferences,
isCloudUser,
isEnterpriseSelfHostedUser,
isAdmin,
isEditor,
]);
// Track if we've done the initial sync (to avoid overwriting user actions during session)
const hasInitializedRef = useRef(false);
// Sync state only on initial load when userPreferences first becomes available
useEffect(() => {
// Only sync once: when userPreferences loads for the first time
if (!hasInitializedRef.current && userPreferences !== null) {
setPinnedMenuItems(computedPinnedMenuItems);
setSecondaryMenuItems(computedSecondaryMenuItems);
hasInitializedRef.current = true;
}
}, [computedPinnedMenuItems, computedSecondaryMenuItems, userPreferences]);
const isOnboardingV3Enabled = featureFlags?.find(
(flag) => flag.name === FeatureKeys.ONBOARDING_V3,
)?.active;
@@ -341,17 +327,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
.map((item) => item.itemKey)
.filter(Boolean) as string[];
// Update context immediately (optimistically) so computed values reflect the change
updateUserPreferenceInContext({
name: USER_PREFERENCES.NAV_SHORTCUTS,
description: USER_PREFERENCES.NAV_SHORTCUTS,
valueType: 'array',
defaultValue: false,
allowedValues: [],
allowedScopes: ['user'],
value: navShortcuts,
});
updateUserPreferenceMutation(
{
name: USER_PREFERENCES.NAV_SHORTCUTS,
@@ -360,7 +335,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
{
onSuccess: (response) => {
if (response.data) {
// Update context again on success to ensure consistency
updateUserPreferenceInContext({
name: USER_PREFERENCES.NAV_SHORTCUTS,
description: USER_PREFERENCES.NAV_SHORTCUTS,
@@ -394,13 +368,13 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
if (isCurrentlyPinned) {
return prevItems.filter((i) => i.key !== item.key);
}
return [...prevItems, item];
return [item, ...prevItems];
});
// Get the updated pinned menu items for preference update
const updatedPinnedItems = pinnedMenuItems.some((i) => i.key === item.key)
? pinnedMenuItems.filter((i) => i.key !== item.key)
: [...pinnedMenuItems, item];
: [item, ...pinnedMenuItems];
// Update user preference with the ordered list of item keys
updateNavShortcutsPreference(updatedPinnedItems);
@@ -481,10 +455,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
pathname,
]);
const isSettingsPage = useMemo(() => pathname.startsWith(ROUTES.SETTINGS), [
pathname,
]);
const userSettingsDropdownMenuItems: MenuProps['items'] = useMemo(
() =>
[
@@ -624,7 +594,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
},
{
type: 'group',
label: "WHAT'S NEW",
label: "WHAT's NEW",
},
...dropdownItems,
{
@@ -780,15 +750,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
[secondaryMenuItems],
);
// Get active "More" items that should be visible in collapsed state
const activeMoreMenuItems = useMemo(
() => moreMenuItems.filter((item) => activeMenuKey === item.key),
[moreMenuItems, activeMenuKey],
);
// Check if sidebar is collapsed (not pinned, not hovered, and no dropdown open)
const isCollapsed = !isPinned && !isHovered && !isDropdownOpen;
const renderNavItems = (
items: SidebarItem[],
allowPin?: boolean,
@@ -940,15 +901,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
return (
<div className={cx('sidenav-container', isPinned && 'pinned')}>
<div
className={cx(
'sideNav',
isPinned && 'pinned',
isDropdownOpen && 'dropdown-open',
)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className={cx('sideNav', isPinned && 'pinned')}>
<div className="brand-container">
<div className="brand">
<div className="brand-company-meta">
@@ -1046,43 +999,35 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
{renderNavItems(primaryMenuItems)}
</div>
{(pinnedMenuItems.length > 0 || !isCollapsed) && (
<div
className={cx('shortcut-nav-items', isCollapsed && 'sidebar-collapsed')}
>
{!isCollapsed && (
<div className="nav-title-section">
<div className="nav-section-title">
<div className="nav-section-title-icon">
<MousePointerClick size={16} />
</div>
<div className="shortcut-nav-items">
<div className="nav-title-section">
<div className="nav-section-title">
<div className="nav-section-title-icon">
<MousePointerClick size={16} />
</div>
<div className="nav-section-title-text">SHORTCUTS</div>
<div className="nav-section-title-text">SHORTCUTS</div>
{pinnedMenuItems.length > 1 && (
<Tooltip title="Manage shortcuts" placement="right">
<div
className="nav-section-title-icon reorder"
onClick={(): void => {
logEvent('Sidebar V2: Manage shortcuts clicked', {});
setIsReorderShortcutNavItemsModalOpen(true);
}}
>
<Logs size={16} />
</div>
</Tooltip>
)}
{pinnedMenuItems.length > 1 && (
<div
className="nav-section-title-icon reorder"
onClick={(): void => {
logEvent('Sidebar V2: Manage shortcuts clicked', {});
setIsReorderShortcutNavItemsModalOpen(true);
}}
>
<Logs size={16} />
</div>
)}
</div>
{pinnedMenuItems.length === 0 && (
<div className="nav-section-subtitle">
You have not added any shortcuts yet.
</div>
)}
{pinnedMenuItems.length === 0 && (
<div className="nav-section-subtitle">
You have not added any shortcuts yet.
</div>
)}
{(pinnedMenuItems.length > 0 || isCollapsed) && (
{pinnedMenuItems.length > 0 && (
<div className="nav-items-section">
{renderNavItems(
pinnedMenuItems.filter((item) => item.isEnabled),
@@ -1091,60 +1036,46 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
</div>
)}
</div>
)}
</div>
{moreMenuItems.length > 0 && (
<div
className={cx(
'more-nav-items',
isMoreMenuCollapsed ? 'collapsed' : 'expanded',
isCollapsed && 'sidebar-collapsed',
)}
>
{!isCollapsed && (
<div className="nav-title-section">
<div
className="nav-section-title"
onClick={(): void => {
// Only allow toggling when sidebar is open (pinned, hovered, or dropdown open)
if (isCollapsed) {
return;
}
const newCollapsedState = !isMoreMenuCollapsed;
logEvent('Sidebar V2: More menu clicked', {
action: isMoreMenuCollapsed ? 'expand' : 'collapse',
});
setIsMoreMenuCollapsed(newCollapsedState);
}}
>
<div className="nav-section-title-icon">
<Ellipsis size={16} />
</div>
<div className="nav-title-section">
<div
className="nav-section-title"
onClick={(): void => {
logEvent('Sidebar V2: More menu clicked', {
action: isMoreMenuCollapsed ? 'expand' : 'collapse',
});
setIsMoreMenuCollapsed(!isMoreMenuCollapsed);
}}
>
<div className="nav-section-title-icon">
<Ellipsis size={16} />
</div>
<div className="nav-section-title-text">MORE</div>
<div className="nav-section-title-text">MORE</div>
<div className="collapse-expand-section-icon">
{isMoreMenuCollapsed ? (
<ChevronDown size={16} />
) : (
<ChevronUp size={16} />
)}
</div>
<div className="collapse-expand-section-icon">
{isMoreMenuCollapsed ? (
<ChevronDown size={16} />
) : (
<ChevronUp size={16} />
)}
</div>
</div>
)}
</div>
<div className="nav-items-section">
{/* Show all items when expanded, only active items when collapsed */}
{isCollapsed
? renderNavItems(
activeMoreMenuItems.filter((item) => item.isEnabled),
true,
)
: renderNavItems(
moreMenuItems.filter((item) => item.isEnabled),
true,
)}
{renderNavItems(
moreMenuItems.filter((item) => item.isEnabled),
true,
)}
</div>
</div>
)}
@@ -1171,7 +1102,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
placement="topLeft"
overlayClassName="nav-dropdown-overlay help-support-dropdown"
trigger={['click']}
onOpenChange={(open): void => setIsDropdownOpen(open)}
>
<div className="nav-item">
<div className="nav-item-data" data-testid="help-support-nav-item">
@@ -1192,10 +1122,8 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
placement="topLeft"
overlayClassName="nav-dropdown-overlay settings-dropdown"
trigger={['click']}
onOpenChange={(open): void => setIsDropdownOpen(open)}
>
<div className={cx('nav-item', isSettingsPage && 'active')}>
<div className="nav-item-active-marker" />
<div className="nav-item">
<div className="nav-item-data" data-testid="settings-nav-item">
<div className="nav-item-icon">{userSettingsMenuItem.icon}</div>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { renderHook } from '@testing-library/react';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariablesStore';
import useGetResolvedText from '../useGetResolvedText';

View File

@@ -1,40 +1,21 @@
import { useCallback, useRef, useSyncExternalStore } from 'react';
import { dashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import { useSyncExternalStore } from 'react';
import {
IDashboardVariablesStoreState,
IUseDashboardVariablesReturn,
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
dashboardVariablesStore,
IDashboardVariables,
} from '../../providers/Dashboard/store/dashboardVariablesStore';
/**
* Generic selector hook for dashboard variables store
* Allows granular subscriptions to any part of the store state
*
* @example
* ! Select top-level field
* const variables = useDashboardVariablesSelector(s => s.variables);
*
* ! Select specific variable
* const fooVar = useDashboardVariablesSelector(s => s.variables['foo']);
*
* ! Select derived value
* const hasVariables = useDashboardVariablesSelector(s => Object.keys(s.variables).length > 0);
*/
export const useDashboardVariablesSelector = <T>(
selector: (state: IDashboardVariablesStoreState) => T,
): T => {
const selectorRef = useRef(selector);
selectorRef.current = selector;
const getSnapshot = useCallback(
() => selectorRef.current(dashboardVariablesStore.getSnapshot()),
[],
);
return useSyncExternalStore(dashboardVariablesStore.subscribe, getSnapshot);
};
export interface IUseDashboardVariablesReturn {
dashboardVariables: IDashboardVariables;
}
export const useDashboardVariables = (): IUseDashboardVariablesReturn => {
const dashboardVariables = useDashboardVariablesSelector((s) => s.variables);
const dashboardVariables = useSyncExternalStore(
dashboardVariablesStore.subscribe,
dashboardVariablesStore.getSnapshot,
);
return { dashboardVariables };
return {
dashboardVariables,
};
};

View File

@@ -1,11 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useState } from 'react';
import {
getPreviousQueryFromKey,
getQueryKey,
removeKeyFromPreviousQuery,
saveAsPreviousQuery,
} from 'components/QueryBuilderV2/QueryV2/previousQuery.utils';
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
import { LEGEND } from 'constants/global';
import {
@@ -36,7 +30,6 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
import {
IBuilderFormula,
IBuilderQuery,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import {
MetricAggregation,
@@ -66,7 +59,6 @@ export const useQueryOperations: UseQueryOperations = ({
isListViewPanel = false,
entityVersion,
isForTraceOperator = false,
savePreviousQuery = false,
}) => {
const {
handleSetQueryData,
@@ -425,50 +417,11 @@ export const useQueryOperations: UseQueryOperations = ({
const handleChangeDataSource = useCallback(
(nextSource: DataSource): void => {
let newQueryData: IBuilderQuery | null = null;
if (savePreviousQuery) {
// save the current query key in session storage
const currKey = getQueryKey({
queryName: query.queryName || '',
dataSource: query.dataSource || '',
signalSource: query.source || '',
panelType: panelType || '',
});
saveAsPreviousQuery(currKey, query);
const newKey = getQueryKey({
queryName: query.queryName || '',
dataSource: nextSource || '',
signalSource: query.source || '',
panelType: panelType || '',
});
newQueryData = getPreviousQueryFromKey(newKey);
// remove the new query key from session storage
removeKeyFromPreviousQuery(newKey);
}
if (isListViewPanel) {
let listPanelQuery: Query | null = null;
if (nextSource === DataSource.LOGS) {
listPanelQuery = listViewInitialLogQuery;
redirectWithQueryBuilderData(listViewInitialLogQuery);
} else if (nextSource === DataSource.TRACES) {
listPanelQuery = listViewInitialTraceQuery;
}
if (newQueryData && listPanelQuery) {
listPanelQuery = {
...listPanelQuery,
builder: {
...listPanelQuery.builder,
queryData: [newQueryData],
},
};
}
if (listPanelQuery) {
redirectWithQueryBuilderData(listPanelQuery);
redirectWithQueryBuilderData(listViewInitialTraceQuery);
}
}
@@ -490,10 +443,8 @@ export const useQueryOperations: UseQueryOperations = ({
aggregateOperator: newOperators[0].value,
};
newQueryData = newQueryData ? newQueryData : newQuery;
setOperators(newOperators);
handleSetQueryData(index, newQueryData);
handleSetQueryData(index, newQuery);
},
[
isListViewPanel,
@@ -502,7 +453,6 @@ export const useQueryOperations: UseQueryOperations = ({
handleSetQueryData,
index,
redirectWithQueryBuilderData,
savePreviousQuery,
],
);

View File

@@ -74,7 +74,7 @@
/>
<% }); %> <% } %>
</head>
<body data-theme="default">
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>

View File

@@ -1,5 +1,5 @@
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariablesStore';
import store from 'store';
export const getDashboardVariables = (

View File

@@ -1,18 +1,7 @@
.legend-search-container {
flex-shrink: 0;
width: 100%;
padding-right: 8px;
.legend-search-input {
font-size: 12px;
}
}
.legend-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
height: 100%;
width: 100%;
@@ -28,38 +17,6 @@
height: 100%;
width: 100%;
.virtuoso-grid-list {
min-width: 0;
display: grid;
grid-auto-flow: row;
grid-template-columns: repeat(
auto-fill,
minmax(var(--legend-average-width, 240px), 1fr)
);
row-gap: 4px;
column-gap: 12px;
}
.virtuoso-grid-item {
min-width: 0;
}
&.legend-virtuoso-container-right {
.virtuoso-grid-list {
grid-template-columns: 1fr;
}
}
&.legend-virtuoso-container-single-row {
.virtuoso-grid-list {
grid-template-columns: repeat(
auto-fit,
minmax(var(--legend-average-width, 240px), max-content)
);
justify-content: center;
}
}
&::-webkit-scrollbar {
width: 0.3rem;
}
@@ -101,15 +58,10 @@
align-items: center;
gap: 6px;
padding: 4px 8px;
max-width: 100%;
overflow: hidden;
border-radius: 4px;
max-width: min(400px, 100%);
cursor: pointer;
&.legend-item-right {
width: 100%;
}
&.legend-item-off {
opacity: 0.3;
text-decoration: line-through;

View File

@@ -1,21 +1,22 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import { VirtuosoGrid } from 'react-virtuoso';
import { Input, Tooltip as AntdTooltip } from 'antd';
import { useCallback, useMemo, useRef } from 'react';
import { Virtuoso } from 'react-virtuoso';
import { Tooltip as AntdTooltip } from 'antd';
import cx from 'classnames';
import { LegendItem } from 'lib/uPlotV2/config/types';
import useLegendsSync from 'lib/uPlotV2/hooks/useLegendsSync';
import { LegendPosition } from 'types/api/dashboard/getAll';
import { LegendPosition, LegendProps } from '../types';
import { LegendProps } from '../types';
import { useLegendActions } from './useLegendActions';
import './Legend.styles.scss';
export const MAX_LEGEND_WIDTH = 240;
const LEGENDS_PER_SET_DEFAULT = 5;
export default function Legend({
position = LegendPosition.BOTTOM,
config,
averageLegendWidth = MAX_LEGEND_WIDTH,
legendsPerSet = LEGENDS_PER_SET_DEFAULT,
}: LegendProps): JSX.Element {
const {
legendItemsMap,
@@ -31,54 +32,53 @@ export default function Legend({
focusedSeriesIndex,
});
const legendContainerRef = useRef<HTMLDivElement | null>(null);
const [legendSearchQuery, setLegendSearchQuery] = useState('');
const legendItems = useMemo(() => Object.values(legendItemsMap), [
legendItemsMap,
]);
// Chunk legend items into rows of LEGENDS_PER_ROW items each
const legendRows = useMemo(() => {
const legendItems = Object.values(legendItemsMap);
const isSingleRow = useMemo(() => {
if (!legendContainerRef.current || position !== LegendPosition.BOTTOM) {
return false;
}
const containerWidth = legendContainerRef.current.clientWidth;
return legendItems.reduce((acc: LegendItem[][], curr, i) => {
if (i % legendsPerSet === 0) {
acc.push([]);
}
acc[acc.length - 1].push(curr);
return acc;
}, [] as LegendItem[][]);
}, [legendItemsMap, legendsPerSet]);
const totalLegendWidth = legendItems.length * (averageLegendWidth + 16);
const totalRows = Math.ceil(totalLegendWidth / containerWidth);
return totalRows <= 1;
}, [averageLegendWidth, legendContainerRef, legendItems.length, position]);
const visibleLegendItems = useMemo(() => {
if (position !== LegendPosition.RIGHT || !legendSearchQuery.trim()) {
return legendItems;
}
const query = legendSearchQuery.trim().toLowerCase();
return legendItems.filter((item) =>
item.label?.toLowerCase().includes(query),
);
}, [position, legendSearchQuery, legendItems]);
const renderLegendItem = useCallback(
(item: LegendItem): JSX.Element => (
<AntdTooltip key={item.seriesIndex} title={item.label}>
<div
data-legend-item-id={item.seriesIndex}
className={cx('legend-item', `legend-item-${position.toLowerCase()}`, {
'legend-item-off': !item.show,
'legend-item-focused': focusedSeriesIndex === item.seriesIndex,
})}
>
<div
className="legend-marker"
style={{ borderColor: String(item.color) }}
data-is-legend-marker={true}
/>
<span className="legend-label">{item.label}</span>
</div>
</AntdTooltip>
const renderLegendRow = useCallback(
(rowIndex: number, row: LegendItem[]): JSX.Element => (
<div
key={rowIndex}
className={cx(
'legend-row',
`legend-row-${position.toLowerCase()}`,
legendRows.length === 1 && position === LegendPosition.BOTTOM
? 'legend-single-row'
: '',
)}
>
{row.map((item) => (
<AntdTooltip key={item.seriesIndex} title={item.label}>
<div
data-legend-item-id={item.seriesIndex}
className={cx('legend-item', {
'legend-item-off': !item.show,
'legend-item-focused': focusedSeriesIndex === item.seriesIndex,
})}
>
<div
className="legend-marker"
style={{ borderColor: String(item.color) }}
data-is-legend-marker={true}
/>
<span className="legend-label">{item.label}</span>
</div>
</AntdTooltip>
))}
</div>
),
[focusedSeriesIndex, position],
[focusedSeriesIndex, position, legendRows],
);
return (
@@ -88,29 +88,11 @@ export default function Legend({
onClick={onLegendClick}
onMouseMove={onLegendMouseMove}
onMouseLeave={onLegendMouseLeave}
style={{
['--legend-average-width' as string]: `${averageLegendWidth + 16}px`, // 16px is the marker width
}}
>
{position === LegendPosition.RIGHT && (
<div className="legend-search-container">
<Input
allowClear
placeholder="Search..."
value={legendSearchQuery}
onChange={(e): void => setLegendSearchQuery(e.target.value)}
className="legend-search-input"
/>
</div>
)}
<VirtuosoGrid
className={cx(
'legend-virtuoso-container',
`legend-virtuoso-container-${position.toLowerCase()}`,
{ 'legend-virtuoso-container-single-row': isSingleRow },
)}
data={visibleLegendItems}
itemContent={(_, item): JSX.Element => renderLegendItem(item)}
<Virtuoso
className="legend-virtuoso-container"
data={legendRows}
itemContent={(index, row): JSX.Element => renderLegendRow(index, row)}
/>
</div>
);

View File

@@ -5,7 +5,7 @@
-webkit-font-smoothing: antialiased;
color: var(--bg-vanilla-100);
border-radius: 6px;
padding: 1rem 0.5rem 0.5rem 1rem;
padding: 1rem 1rem 0.5rem 1rem;
border: 1px solid var(--bg-ink-100);
display: flex;
flex-direction: column;
@@ -15,12 +15,6 @@
background: var(--bg-vanilla-100);
color: var(--bg-ink-500);
border: 1px solid var(--bg-vanilla-300);
.uplot-tooltip-list {
&::-webkit-scrollbar-thumb {
background: var(--bg-vanilla-400);
}
}
}
.uplot-tooltip-header {
@@ -28,18 +22,18 @@
font-weight: 500;
}
.uplot-tooltip-list {
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-100);
border-radius: 0.5rem;
.uplot-tooltip-list-container {
height: 100%;
.uplot-tooltip-list {
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
}
}
}

View File

@@ -39,7 +39,7 @@ export default function Tooltip({
data: uPlotInstance.data,
series: uPlotInstance.series,
dataIndexes,
activeSeriesIndex: seriesIndex,
activeSeriesIdx: seriesIndex,
uPlotInstance,
yAxisUnit,
decimalPrecision,

View File

@@ -9,10 +9,10 @@ const FALLBACK_SERIES_COLOR = '#000000';
export function resolveSeriesColor(
stroke: Series.Stroke | undefined,
u: uPlot,
seriesIndex: number,
seriesIdx: number,
): string {
if (typeof stroke === 'function') {
return String(stroke(u, seriesIndex));
return String(stroke(u, seriesIdx));
}
if (typeof stroke === 'string') {
return stroke;
@@ -24,7 +24,7 @@ export function buildTooltipContent({
data,
series,
dataIndexes,
activeSeriesIndex,
activeSeriesIdx,
uPlotInstance,
yAxisUnit,
decimalPrecision,
@@ -32,7 +32,7 @@ export function buildTooltipContent({
data: AlignedData;
series: Series[];
dataIndexes: Array<number | null>;
activeSeriesIndex: number | null;
activeSeriesIdx: number | null;
uPlotInstance: uPlot;
yAxisUnit: string;
decimalPrecision?: PrecisionOption;
@@ -40,28 +40,28 @@ export function buildTooltipContent({
const active: TooltipContentItem[] = [];
const rest: TooltipContentItem[] = [];
for (let index = 1; index < series.length; index += 1) {
const s = series[index];
for (let idx = 1; idx < series.length; idx += 1) {
const s = series[idx];
if (!s?.show) {
continue;
}
const dataIndex = dataIndexes[index];
const dataIdx = dataIndexes[idx];
// Skip series with no data at the current cursor position
if (dataIndex === null) {
if (dataIdx === null) {
continue;
}
const raw = data[index]?.[dataIndex];
const raw = data[idx]?.[dataIdx];
const value = Number(raw);
const displayValue = Number.isNaN(value) ? 0 : value;
const isActive = index === activeSeriesIndex;
const isActive = idx === activeSeriesIdx;
const item: TooltipContentItem = {
label: String(s.label ?? ''),
value: displayValue,
tooltipValue: getToolTipValue(displayValue, yAxisUnit, decimalPrecision),
color: resolveSeriesColor(s.stroke, uPlotInstance, index),
color: resolveSeriesColor(s.stroke, uPlotInstance, idx),
isActive,
};

View File

@@ -6,6 +6,7 @@ import { LineChart } from 'lucide-react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import uPlot, { AlignedData, Options } from 'uplot';
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
import { usePlotContext } from '../context/PlotContext';
import { UPlotChartProps } from './types';
@@ -48,6 +49,7 @@ export default function UPlotChart({
const containerRef = useRef<HTMLDivElement>(null);
const plotInstanceRef = useRef<uPlot | null>(null);
const prevPropsRef = useRef<UPlotChartProps | null>(null);
const configUsedForPlotRef = useRef<UPlotConfigBuilder | null>(null);
/**
* Destroy the existing plot instance if present.
@@ -55,6 +57,12 @@ export default function UPlotChart({
const destroyPlot = useCallback((): void => {
if (plotInstanceRef.current) {
onDestroy?.(plotInstanceRef.current);
// Clean up the config builder that was used to create this plot (not the current prop)
if (configUsedForPlotRef.current) {
configUsedForPlotRef.current.destroy();
}
configUsedForPlotRef.current = null;
plotInstanceRef.current.destroy();
plotInstanceRef.current = null;
setPlotContextInitialState({ uPlotInstance: null });
@@ -89,14 +97,13 @@ export default function UPlotChart({
if (plotRef) {
plotRef(plot);
}
setPlotContextInitialState({
uPlotInstance: plot,
widgetId: config.getWidgetId(),
shouldSaveSelectionPreference: config.getShouldSaveSelectionPreference(),
});
plotInstanceRef.current = plot;
configUsedForPlotRef.current = config;
}, [
config,
data,
@@ -136,13 +143,11 @@ export default function UPlotChart({
return;
}
// Check if the plot instance's root is no longer attached to our container
// (e.g., after React has remounted the container div). In uPlot, `root`
// is a child of the container, so we must compare against its parent node.
// Check if the plot instance's container has been unmounted (e.g., after "No Data" state)
// If so, we need to recreate the plot with the new container
const isPlotOrphaned =
!!plotInstanceRef.current &&
(!containerRef.current ||
plotInstanceRef.current.root.parentElement !== containerRef.current);
plotInstanceRef.current &&
plotInstanceRef.current.root !== containerRef.current;
// Update dimensions without reinitializing if only size changed
if (

View File

@@ -75,7 +75,7 @@ export interface LegendConfig {
export interface LegendProps {
position?: LegendPosition;
config: UPlotConfigBuilder;
averageLegendWidth?: number;
legendsPerSet?: number;
}
export interface TooltipContentItem {

View File

@@ -1,7 +1,6 @@
import { getStoredSeriesVisibility } from 'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils';
import { ThresholdsDrawHookOptions } from 'lib/uPlotV2/hooks/types';
import { thresholdsDrawHook } from 'lib/uPlotV2/hooks/useThresholdsDrawHook';
import { resolveSeriesVisibility } from 'lib/uPlotV2/utils/seriesVisibility';
import { merge } from 'lodash-es';
import noop from 'lodash-es/noop';
import uPlot, { Cursor, Hooks, Options } from 'uplot';
@@ -12,7 +11,6 @@ import {
DEFAULT_CURSOR_CONFIG,
DEFAULT_PLOT_CONFIG,
LegendItem,
SelectionPreferencesSource,
} from './types';
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
import { ScaleProps, UPlotScaleBuilder } from './UPlotScaleBuilder';
@@ -38,10 +36,6 @@ export class UPlotConfigBuilder extends ConfigBuilder<
> {
series: UPlotSeriesBuilder[] = [];
private selectionPreferencesSource: SelectionPreferencesSource =
SelectionPreferencesSource.IN_MEMORY;
private shouldSaveSelectionPreference = false;
private axes: Record<string, UPlotAxisBuilder> = {};
readonly scales: UPlotScaleBuilder[] = [];
@@ -70,15 +64,11 @@ export class UPlotConfigBuilder extends ConfigBuilder<
private onDragSelect: (startTime: number, endTime: number) => void;
private cleanups: Array<() => void> = [];
constructor(args?: ConfigBuilderProps) {
super(args ?? {});
const {
widgetId,
onDragSelect,
tzDate,
selectionPreferencesSource,
shouldSaveSelectionPreference,
} = args ?? {};
const { widgetId, onDragSelect, tzDate } = args ?? {};
if (widgetId) {
this.widgetId = widgetId;
}
@@ -87,38 +77,25 @@ export class UPlotConfigBuilder extends ConfigBuilder<
this.tzDate = tzDate;
}
if (selectionPreferencesSource) {
this.selectionPreferencesSource = selectionPreferencesSource;
}
if (shouldSaveSelectionPreference) {
this.shouldSaveSelectionPreference = shouldSaveSelectionPreference;
}
this.onDragSelect = noop;
if (onDragSelect) {
this.onDragSelect = onDragSelect;
this.addHook('setSelect', (self: uPlot): void => {
// Add a hook to handle the select event
const cleanup = this.addHook('setSelect', (self: uPlot): void => {
const selection = self.select;
// Only trigger onDragSelect when there's an actual drag range (width > 0)
// A click without dragging produces width === 0, which should be ignored
if (selection && selection.width > 0) {
const startTime = self.posToVal(selection.left, 'x');
const endTime = self.posToVal(selection.left + selection.width, 'x');
this.onDragSelect(startTime * 1000, endTime * 1000);
}
});
this.cleanups.push(cleanup);
}
}
/**
* Get the save selection preferences
*/
getShouldSaveSelectionPreference(): boolean {
return this.shouldSaveSelectionPreference;
}
/**
* Add or merge an axis configuration
*/
@@ -181,7 +158,8 @@ export class UPlotConfigBuilder extends ConfigBuilder<
addThresholds(options: ThresholdsDrawHookOptions): void {
if (!this.thresholds[options.scaleKey]) {
this.thresholds[options.scaleKey] = options;
this.addHook('draw', thresholdsDrawHook(options));
const cleanup = this.addHook('draw', thresholdsDrawHook(options));
this.cleanups.push(cleanup);
}
}
@@ -234,39 +212,20 @@ export class UPlotConfigBuilder extends ConfigBuilder<
this.tzDate = tzDate;
}
/**
* Returns stored series visibility map from localStorage when preferences source is LOCAL_STORAGE, otherwise null.
*/
private getStoredVisibilityMap(): Map<string, boolean> | null {
if (
this.widgetId &&
this.selectionPreferencesSource === SelectionPreferencesSource.LOCAL_STORAGE
) {
return getStoredSeriesVisibility(this.widgetId);
}
return null;
}
/**
* Get legend items with visibility state restored from localStorage if available
*/
getLegendItems(): Record<number, LegendItem> {
const visibilityMap = this.getStoredVisibilityMap();
const isAnySeriesHidden = !!(
visibilityMap && Array.from(visibilityMap.values()).some((show) => !show)
);
const visibilityMap = this.widgetId
? getStoredSeriesVisibility(this.widgetId)
: null;
return this.series.reduce((acc, s: UPlotSeriesBuilder, index: number) => {
const seriesConfig = s.getConfig();
const label = seriesConfig.label ?? '';
const seriesIndex = index + 1; // +1 because the first series is the timestamp
const show = resolveSeriesVisibility(
label,
seriesConfig.show,
visibilityMap,
isAnySeriesHidden,
);
// Priority: stored visibility > series config > default (true)
const show = visibilityMap?.get(label) ?? seriesConfig.show ?? true;
acc[seriesIndex] = {
seriesIndex,
@@ -279,6 +238,13 @@ export class UPlotConfigBuilder extends ConfigBuilder<
}, {} as Record<number, LegendItem>);
}
/**
* Remove all hooks and cleanup functions
*/
destroy(): void {
this.cleanups.forEach((cleanup) => cleanup());
}
/**
* Get the widget id
*/
@@ -294,27 +260,9 @@ export class UPlotConfigBuilder extends ConfigBuilder<
...DEFAULT_PLOT_CONFIG,
};
const visibilityMap = this.getStoredVisibilityMap();
const isAnySeriesHidden = !!(
visibilityMap && Array.from(visibilityMap.values()).some((show) => !show)
);
config.series = [
{ value: (): string => '' }, // Base series for timestamp
...this.series.map((s) => {
const series = s.getConfig();
const label = series.label ?? '';
const visible = resolveSeriesVisibility(
label,
series.show,
visibilityMap,
isAnySeriesHidden,
);
return {
...series,
show: visible,
};
}),
...this.series.map((s) => s.getConfig()),
];
config.axes = Object.values(this.axes).map((a) => a.getConfig());
config.scales = this.scales.reduce(

View File

@@ -40,8 +40,8 @@ export class UPlotScaleBuilder extends ConfigBuilder<
range,
thresholds,
logBase = 10,
padMinBy = 0,
padMaxBy = 0.05,
padMinBy = 0.1,
padMaxBy = 0.1,
} = this.props;
// Special handling for time scales (X axis)

View File

@@ -5,8 +5,8 @@ import uPlot, { Series } from 'uplot';
import {
ConfigBuilder,
DrawStyle,
FillStyle,
LineInterpolation,
LineStyle,
SeriesProps,
VisibilityMode,
} from './types';
@@ -16,29 +16,23 @@ import {
* Handles creation of series settings
*/
export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
private buildLineConfig({
lineColor,
lineWidth,
lineStyle,
lineCap,
}: {
lineColor: string;
lineWidth?: number;
lineStyle?: LineStyle;
lineCap?: Series.Cap;
}): Partial<Series> {
private buildLineConfig(
lineColor: string,
lineWidth?: number,
lineStyle?: { fill?: FillStyle; dash?: number[] },
): Partial<Series> {
const lineConfig: Partial<Series> = {
stroke: lineColor,
width: lineWidth ?? 2,
};
if (lineStyle === LineStyle.Dashed) {
lineConfig.dash = [10, 10];
if (lineStyle && lineStyle.fill !== FillStyle.Solid) {
if (lineStyle.fill === FillStyle.Dot) {
lineConfig.cap = 'round';
}
lineConfig.dash = lineStyle.dash ?? [10, 10];
}
if (lineCap) {
lineConfig.cap = lineCap;
}
return lineConfig;
}
@@ -144,7 +138,6 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
lineInterpolation,
lineWidth,
lineStyle,
lineCap,
showPoints,
pointSize,
scaleKey,
@@ -155,12 +148,7 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
const lineColor = this.getLineColor();
const lineConfig = this.buildLineConfig({
lineColor,
lineWidth,
lineStyle,
lineCap,
});
const lineConfig = this.buildLineConfig(lineColor, lineWidth, lineStyle);
const pathConfig = this.buildPathConfig({
pathBuilder,
drawStyle,

View File

@@ -22,14 +22,6 @@ export abstract class ConfigBuilder<P, T> {
merge?(props: Partial<P>): void;
}
/**
* Preferences source for the uPlot config builder
*/
export enum SelectionPreferencesSource {
LOCAL_STORAGE = 'LOCAL_STORAGE',
IN_MEMORY = 'IN_MEMORY',
}
/**
* Props for configuring the uPlot config builder
*/
@@ -37,8 +29,6 @@ export interface ConfigBuilderProps {
widgetId?: string;
onDragSelect?: (startTime: number, endTime: number) => void;
tzDate?: uPlot.LocalDateFromUnix;
selectionPreferencesSource?: SelectionPreferencesSource;
shouldSaveSelectionPreference?: boolean;
}
/**
@@ -102,9 +92,16 @@ export interface ScaleProps {
* Props for configuring a series
*/
export enum LineStyle {
export enum FillStyle {
Solid = 'solid',
Dashed = 'dashed',
Dash = 'dash',
Dot = 'dot',
Square = 'square',
}
export interface LineStyle {
dash?: Array<number>;
fill?: FillStyle;
}
export enum DrawStyle {
@@ -144,7 +141,6 @@ export interface SeriesProps {
lineInterpolation?: LineInterpolation;
lineStyle?: LineStyle;
lineWidth?: number;
lineCap?: Series.Cap;
// Points config
pointColor?: string;

View File

@@ -9,17 +9,16 @@ import {
import type { SeriesVisibilityItem } from 'container/DashboardContainer/visualization/panels/types';
import { updateSeriesVisibilityToLocalStorage } from 'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils';
import type uPlot from 'uplot';
export interface PlotContextInitialState {
uPlotInstance: uPlot | null;
widgetId?: string;
shouldSaveSelectionPreference?: boolean;
}
export interface IPlotContext {
setPlotContextInitialState: (state: PlotContextInitialState) => void;
onToggleSeriesVisibility: (seriesIndex: number) => void;
onToggleSeriesOnOff: (seriesIndex: number) => void;
onFocusSeries: (seriesIndex: number | null) => void;
syncSeriesVisibilityToLocalStorage: () => void;
}
export const PlotContext = createContext<IPlotContext | null>(null);
@@ -30,84 +29,68 @@ export const PlotContextProvider = ({
const uPlotInstanceRef = useRef<uPlot | null>(null);
const activeSeriesIndex = useRef<number | undefined>(undefined);
const widgetIdRef = useRef<string | undefined>(undefined);
const shouldSavePreferencesRef = useRef<boolean>(false);
const setPlotContextInitialState = useCallback(
({
uPlotInstance,
widgetId,
shouldSaveSelectionPreference,
}: PlotContextInitialState): void => {
({ uPlotInstance, widgetId }: PlotContextInitialState): void => {
uPlotInstanceRef.current = uPlotInstance;
widgetIdRef.current = widgetId;
activeSeriesIndex.current = undefined;
shouldSavePreferencesRef.current = !!shouldSaveSelectionPreference;
},
[],
);
const syncSeriesVisibilityToLocalStorage = useCallback((): void => {
const onToggleSeriesVisibility = useCallback((seriesIndex: number): void => {
const plot = uPlotInstanceRef.current;
if (!plot || !widgetIdRef.current) {
if (!plot) {
return;
}
const seriesVisibility: SeriesVisibilityItem[] = plot.series.map(
(series) => ({
label: series.label ?? '',
show: series.show ?? true,
}),
);
const isReset = activeSeriesIndex.current === seriesIndex;
activeSeriesIndex.current = isReset ? undefined : seriesIndex;
updateSeriesVisibilityToLocalStorage(widgetIdRef.current, seriesVisibility);
plot.batch(() => {
plot.series.forEach((_, index) => {
if (index === 0) {
return;
}
const currentSeriesIndex = index;
plot.setSeries(currentSeriesIndex, {
show: isReset || currentSeriesIndex === seriesIndex,
});
});
if (widgetIdRef.current) {
const seriesVisibility: SeriesVisibilityItem[] = plot.series.map(
(series) => ({
label: series.label ?? '',
show: series.show ?? true,
}),
);
updateSeriesVisibilityToLocalStorage(widgetIdRef.current, seriesVisibility);
}
});
}, []);
const onToggleSeriesVisibility = useCallback(
(seriesIndex: number): void => {
const plot = uPlotInstanceRef.current;
if (!plot) {
return;
}
const onToggleSeriesOnOff = useCallback((seriesIndex: number): void => {
const plot = uPlotInstanceRef.current;
if (!plot) {
return;
}
const isReset = activeSeriesIndex.current === seriesIndex;
activeSeriesIndex.current = isReset ? undefined : seriesIndex;
plot.batch(() => {
plot.series.forEach((_, index) => {
if (index === 0) {
return;
}
const currentSeriesIndex = index;
plot.setSeries(currentSeriesIndex, {
show: isReset || currentSeriesIndex === seriesIndex,
});
});
if (widgetIdRef.current && shouldSavePreferencesRef.current) {
syncSeriesVisibilityToLocalStorage();
}
});
},
[syncSeriesVisibilityToLocalStorage],
);
const onToggleSeriesOnOff = useCallback(
(seriesIndex: number): void => {
const plot = uPlotInstanceRef.current;
if (!plot) {
return;
}
const series = plot.series[seriesIndex];
if (!series) {
return;
}
plot.setSeries(seriesIndex, { show: !series.show });
if (widgetIdRef.current && shouldSavePreferencesRef.current) {
syncSeriesVisibilityToLocalStorage();
}
},
[syncSeriesVisibilityToLocalStorage],
);
const series = plot.series[seriesIndex];
if (!series) {
return;
}
plot.setSeries(seriesIndex, { show: !series.show });
if (widgetIdRef.current) {
const seriesVisibility: SeriesVisibilityItem[] = plot.series.map(
(series) => ({
label: series.label ?? '',
show: series.show ?? true,
}),
);
updateSeriesVisibilityToLocalStorage(widgetIdRef.current, seriesVisibility);
}
}, []);
const onFocusSeries = useCallback((seriesIndex: number | null): void => {
const plot = uPlotInstanceRef.current;
@@ -130,14 +113,12 @@ export const PlotContextProvider = ({
setPlotContextInitialState,
onToggleSeriesOnOff,
onFocusSeries,
syncSeriesVisibilityToLocalStorage,
}),
[
onToggleSeriesVisibility,
setPlotContextInitialState,
onToggleSeriesOnOff,
onFocusSeries,
syncSeriesVisibilityToLocalStorage,
],
);

View File

@@ -1,14 +0,0 @@
.tooltip-plugin-container {
top: 0;
left: 0;
z-index: 1070;
white-space: pre;
border-radius: 4px;
position: fixed;
overflow: auto;
transform: translate(-1000px, -1000px); // hide the tooltip initially
&.pinned {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
}

View File

@@ -1,375 +0,0 @@
import { useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import cx from 'classnames';
import uPlot from 'uplot';
import {
createInitialControllerState,
createSetCursorHandler,
createSetLegendHandler,
createSetSeriesHandler,
isScrollEventInPlot,
updatePlotVisibility,
updateWindowSize,
} from './tooltipController';
import {
DashboardCursorSync,
TooltipControllerContext,
TooltipControllerState,
TooltipLayoutInfo,
TooltipPluginProps,
TooltipViewState,
} from './types';
import { createInitialViewState, createLayoutObserver } from './utils';
import './TooltipPlugin.styles.scss';
const INTERACTIVE_CONTAINER_CLASSNAME = '.tooltip-plugin-container';
// Delay before hiding an unpinned tooltip when the cursor briefly leaves
// the plot this avoids flicker when moving between nearby points.
const HOVER_DISMISS_DELAY_MS = 100;
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function TooltipPlugin({
config,
render,
maxWidth = 300,
maxHeight = 400,
syncMode = DashboardCursorSync.None,
syncKey = '_tooltip_sync_global_',
canPinTooltip = false,
}: TooltipPluginProps): JSX.Element | null {
const containerRef = useRef<HTMLDivElement>(null);
const portalRoot = useRef<HTMLElement>(document.body);
const rafId = useRef<number | null>(null);
const dismissTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
const layoutRef = useRef<TooltipLayoutInfo>();
const renderRef = useRef(render);
renderRef.current = render;
// React-managed snapshot of what should be rendered. The controller
// owns the interaction state and calls `updateState` when a visible
// change should trigger a React re-render.
const [viewState, setState] = useState<TooltipViewState>(
createInitialViewState,
);
const { plot, isHovering, isPinned, contents, style } = viewState;
/**
* Merge a partial view update into the current React state.
* Style is merged shallowly so callers can update transform /
* pointerEvents without having to rebuild the whole object.
*/
function updateState(updates: Partial<TooltipViewState>): void {
setState((prev) => ({
...prev,
...updates,
style: { ...prev.style, ...updates.style },
}));
}
useLayoutEffect((): (() => void) => {
layoutRef.current?.observer.disconnect();
layoutRef.current = createLayoutObserver(layoutRef);
// Controller holds the mutable interaction state for this tooltip
// instance. It is intentionally *not* React state so uPlot hooks
// and DOM listeners can update it freely without triggering a
// render on every mouse move.
const controller: TooltipControllerState = createInitialControllerState();
const syncTooltipWithDashboard = syncMode === DashboardCursorSync.Tooltip;
// Enable uPlot's built-in cursor sync when requested so that
// crosshair / tooltip can follow the dashboard-wide cursor.
if (syncMode !== DashboardCursorSync.None && config.scales[0]?.props.time) {
config.setCursor({
sync: { key: syncKey, scales: ['x', null] },
});
}
// Dismiss the tooltip when the user clicks / presses a key
// outside the tooltip container while it is pinned.
const onOutsideInteraction = (event: Event): void => {
const target = event.target as HTMLElement;
if (!target.closest(INTERACTIVE_CONTAINER_CLASSNAME)) {
dismissTooltip();
}
};
// When pinned we want the tooltip to be mouse-interactive
// (for copying values etc.), otherwise it should ignore
// pointer events so the chart remains fully clickable.
function updatePointerEvents(): void {
controller.style = {
...controller.style,
pointerEvents: controller.pinned ? 'all' : 'none',
};
}
// Lock uPlot's internal cursor when the tooltip is pinned so
// subsequent mouse moves do not move the crosshair.
function updateCursorLock(): void {
if (controller.plot) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore uPlot cursor lock is not working as expected
controller.plot.cursor._lock = controller.pinned;
}
}
// Attach / detach global listeners when pin state changes so
// we can detect when the user interacts outside the tooltip.
function toggleOutsideListeners(enable: boolean): void {
if (enable) {
document.addEventListener('mousedown', onOutsideInteraction, true);
document.addEventListener('keydown', onOutsideInteraction, true);
} else {
document.removeEventListener('mousedown', onOutsideInteraction, true);
document.removeEventListener('keydown', onOutsideInteraction, true);
}
}
// Centralised helper that applies all side effects that depend
// on whether the tooltip is currently pinned.
function applyPinnedSideEffects(): void {
updatePointerEvents();
updateCursorLock();
toggleOutsideListeners(controller.pinned);
}
// Hide the tooltip and reset the uPlot cursor. This is used
// both when the user unpins and when interaction ends.
function dismissTooltip(): void {
const isPinnedBeforeDismiss = controller.pinned;
controller.pinned = false;
controller.hoverActive = false;
if (controller.plot) {
controller.plot.setCursor({ left: -10, top: -10 });
}
scheduleRender(isPinnedBeforeDismiss);
}
// Build the React node to be rendered inside the tooltip by
// delegating to the caller-provided `render` function.
function createTooltipContents(): React.ReactNode {
if (!controller.hoverActive || !controller.plot) {
return null;
}
return renderRef.current({
uPlotInstance: controller.plot,
dataIndexes: controller.seriesIndexes,
seriesIndex: controller.focusedSeriesIndex,
isPinned: controller.pinned,
dismiss: dismissTooltip,
viaSync: controller.cursorDrivenBySync,
});
}
// Push the latest controller state into React so the tooltip's
// DOM representation catches up with the interaction state.
function performRender(): void {
rafId.current = null;
dismissTimeoutId.current = null;
if (controller.pendingPinnedUpdate) {
applyPinnedSideEffects();
controller.pendingPinnedUpdate = false;
}
updateState({
style: controller.style,
isPinned: controller.pinned,
isHovering: controller.hoverActive,
contents: createTooltipContents(),
dismiss: dismissTooltip,
});
}
// Cancel any pending render to prevent race conditions
function cancelPendingRender(): void {
if (rafId.current != null) {
cancelAnimationFrame(rafId.current);
rafId.current = null;
}
if (dismissTimeoutId.current != null) {
clearTimeout(dismissTimeoutId.current);
dismissTimeoutId.current = null;
}
}
// Throttle React re-renders:
// - use rAF while hovering for smooth updates
// - use a small timeout when hiding to avoid flicker when
// briefly leaving and re-entering the plot.
//
// Re-entering hover while a dismiss timeout is pending should
// cancel that timeout so it cannot fire with stale state.
function scheduleRender(updatePinned = false): void {
// Always cancel any existing pending callback first so that
// a newly scheduled render reflects the latest controller
// state (e.g. when quickly re-entering after a brief leave).
cancelPendingRender();
if (controller.hoverActive) {
rafId.current = requestAnimationFrame(performRender);
} else {
dismissTimeoutId.current = setTimeout(
performRender,
HOVER_DISMISS_DELAY_MS,
);
}
if (updatePinned) {
controller.pendingPinnedUpdate = true;
}
}
// Keep controller's windowWidth / windowHeight in sync so that
// tooltip positioning can respect the current viewport size.
const handleWindowResize = (): void => {
updateWindowSize(controller);
};
// When the user scrolls, recompute plot visibility and hide
// the tooltip if the scroll originated from inside the plot.
const handleScroll = (event: Event): void => {
updatePlotVisibility(controller);
if (controller.hoverActive && isScrollEventInPlot(event, controller)) {
dismissTooltip();
}
};
// When pinning is enabled, a click on the plot overlay while
// hovering converts the transient tooltip into a pinned one.
const handleUPlotOverClick = (u: uPlot, event: MouseEvent): void => {
if (
event.target === u.over &&
controller.hoverActive &&
!controller.pinned &&
controller.focusedSeriesIndex != null
) {
setTimeout(() => {
controller.pinned = true;
scheduleRender(true);
}, 0);
}
};
let overClickHandler: ((event: MouseEvent) => void) | null = null;
// Called once per uPlot instance; used to store the instance
// on the controller and optionally attach the pinning handler.
const handleInit = (u: uPlot): void => {
controller.plot = u;
updateState({ plot: u });
if (canPinTooltip) {
overClickHandler = (event: MouseEvent): void =>
handleUPlotOverClick(u, event);
u.over.addEventListener('click', overClickHandler);
}
};
// If the underlying data changes we drop any pinned tooltip,
// since the contents may no longer match the new series data.
const handleSetData = (): void => {
if (controller.pinned) {
dismissTooltip();
}
};
// Shared context object passed down into all uPlot hook
// handlers so they can interact with the controller and
// schedule React updates when needed.
const ctx: TooltipControllerContext = {
controller,
layoutRef,
containerRef,
rafId,
updateState,
renderRef,
syncMode,
syncKey,
canPinTooltip,
createTooltipContents,
scheduleRender,
dismissTooltip,
};
const handleSetSeries = createSetSeriesHandler(ctx, syncTooltipWithDashboard);
const handleSetLegend = createSetLegendHandler(ctx, syncTooltipWithDashboard);
const handleSetCursor = createSetCursorHandler(ctx);
handleWindowResize();
const removeReadyHook = config.addHook('ready', (): void =>
updatePlotVisibility(controller),
);
const removeInitHook = config.addHook('init', handleInit);
const removeSetDataHook = config.addHook('setData', handleSetData);
const removeSetSeriesHook = config.addHook('setSeries', handleSetSeries);
const removeSetLegendHook = config.addHook('setLegend', handleSetLegend);
const removeSetCursorHook = config.addHook('setCursor', handleSetCursor);
window.addEventListener('resize', handleWindowResize);
window.addEventListener('scroll', handleScroll, true);
return (): void => {
layoutRef.current?.observer.disconnect();
window.removeEventListener('resize', handleWindowResize);
window.removeEventListener('scroll', handleScroll, true);
document.removeEventListener('mousedown', onOutsideInteraction, true);
document.removeEventListener('keydown', onOutsideInteraction, true);
cancelPendingRender();
removeReadyHook();
removeInitHook();
removeSetDataHook();
removeSetSeriesHook();
removeSetLegendHook();
removeSetCursorHook();
if (controller.plot && overClickHandler) {
controller.plot.over.removeEventListener('click', overClickHandler);
overClickHandler = null;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);
useLayoutEffect((): void => {
if (!plot || !layoutRef.current) {
return;
}
const layout = layoutRef.current;
if (containerRef.current) {
layout.observer.disconnect();
layout.observer.observe(containerRef.current);
const { width, height } = containerRef.current.getBoundingClientRect();
layout.width = width;
layout.height = height;
} else {
layout.width = 0;
layout.height = 0;
}
}, [isHovering, plot]);
if (!plot || !isHovering) {
return null;
}
return createPortal(
<div
className={cx('tooltip-plugin-container', { pinned: isPinned })}
style={{
...style,
maxWidth: `${maxWidth}px`,
maxHeight: `${maxHeight}px`,
width: '100%',
}}
aria-live="polite"
aria-atomic="true"
ref={containerRef}
>
{contents}
</div>,
portalRoot.current,
);
}

View File

@@ -1,218 +0,0 @@
import { isEqual } from 'lodash-es';
import uPlot from 'uplot';
import { TooltipControllerContext, TooltipControllerState } from './types';
import {
buildTransform,
calculateTooltipOffset,
isPlotInViewport,
} from './utils';
const WINDOW_OFFSET = 16;
export function createInitialControllerState(): TooltipControllerState {
return {
plot: null,
hoverActive: false,
isAnySeriesActive: false,
pinned: false,
style: { transform: '', pointerEvents: 'none' },
horizontalOffset: 0,
verticalOffset: 0,
seriesIndexes: [],
focusedSeriesIndex: null,
cursorDrivenBySync: false,
plotWithinViewport: false,
windowWidth: window.innerWidth - WINDOW_OFFSET,
windowHeight: window.innerHeight - WINDOW_OFFSET,
pendingPinnedUpdate: false,
};
}
/**
* Keep track of the current window size and clear hover state
* when the user resizes while hovering (to avoid an orphan tooltip).
*/
export function updateWindowSize(controller: TooltipControllerState): void {
if (controller.hoverActive && !controller.pinned) {
controller.hoverActive = false;
}
controller.windowWidth = window.innerWidth - WINDOW_OFFSET;
controller.windowHeight = window.innerHeight - WINDOW_OFFSET;
}
/**
* Mark whether the plot is currently inside the viewport.
* This is used to decide if a synced tooltip should be shown at all.
*/
export function updatePlotVisibility(controller: TooltipControllerState): void {
if (!controller.plot) {
controller.plotWithinViewport = false;
return;
}
controller.plotWithinViewport = isPlotInViewport(
controller.plot.rect,
controller.windowWidth,
controller.windowHeight,
);
}
/**
* Helper to detect whether a scroll event actually happened inside
* the plot container. Used so we only dismiss the tooltip when the
* user scrolls the chart, not the whole page.
*/
export function isScrollEventInPlot(
event: Event,
controller: TooltipControllerState,
): boolean {
return (
event.target instanceof Node &&
controller.plot !== null &&
event.target.contains(controller.plot.root)
);
}
export function shouldShowTooltipForSync(
controller: TooltipControllerState,
syncTooltipWithDashboard: boolean,
): boolean {
return (
controller.plotWithinViewport &&
controller.isAnySeriesActive &&
syncTooltipWithDashboard
);
}
export function shouldShowTooltipForInteraction(
controller: TooltipControllerState,
): boolean {
return controller.focusedSeriesIndex != null || controller.isAnySeriesActive;
}
export function updateHoverState(
controller: TooltipControllerState,
syncTooltipWithDashboard: boolean,
): void {
// When the cursor is driven by dashboardlevel sync, we only show
// the tooltip if the plot is in viewport and at least one series
// is active. Otherwise we fall back to local interaction logic.
controller.hoverActive = controller.cursorDrivenBySync
? shouldShowTooltipForSync(controller, syncTooltipWithDashboard)
: shouldShowTooltipForInteraction(controller);
}
export function createSetCursorHandler(
ctx: TooltipControllerContext,
): (u: uPlot) => void {
return (u: uPlot): void => {
const { controller, layoutRef, containerRef } = ctx;
controller.cursorDrivenBySync = u.cursor.event == null;
if (!controller.hoverActive) {
return;
}
const { left = -10, top = -10 } = u.cursor;
if (left < 0 && top < 0) {
return;
}
const clientX = u.rect.left + left;
const clientY = u.rect.top + top;
const layout = layoutRef.current;
if (!layout) {
return;
}
const { width: layoutWidth, height: layoutHeight } = layout;
const offsets = calculateTooltipOffset(
clientX,
clientY,
layoutWidth,
layoutHeight,
controller.horizontalOffset,
controller.verticalOffset,
controller.windowWidth,
controller.windowHeight,
);
controller.horizontalOffset = offsets.horizontalOffset;
controller.verticalOffset = offsets.verticalOffset;
const transform = buildTransform(
clientX,
clientY,
controller.horizontalOffset,
controller.verticalOffset,
);
// If the DOM node is mounted we move it directly to avoid
// going through React; otherwise we cache the transform in
// controller.style and ask the plugin to rerender.
if (containerRef.current) {
containerRef.current.style.transform = transform;
} else {
controller.style = { ...controller.style, transform };
ctx.scheduleRender();
}
};
}
export function createSetLegendHandler(
ctx: TooltipControllerContext,
syncTooltipWithDashboard: boolean,
): (u: uPlot) => void {
return (u: uPlot): void => {
const { controller } = ctx;
if (!controller.plot?.cursor?.idxs) {
return;
}
const newSeriesIndexes = controller.plot.cursor.idxs.slice();
const isAnySeriesActive = newSeriesIndexes.some((v, i) => i > 0 && v != null);
const previousCursorDrivenBySync = controller.cursorDrivenBySync;
// Skip scheduling if legend data is unchanged
const seriesIndexesChanged = !isEqual(
controller.seriesIndexes,
newSeriesIndexes,
);
controller.seriesIndexes = newSeriesIndexes;
controller.isAnySeriesActive = isAnySeriesActive;
controller.cursorDrivenBySync = u.cursor.event == null;
const previousHover = controller.hoverActive;
updateHoverState(controller, syncTooltipWithDashboard);
const hoverStateChanged = controller.hoverActive !== previousHover;
const cursorDrivenBySyncChanged =
controller.cursorDrivenBySync !== previousCursorDrivenBySync;
// Only schedule when legend data, hover state, or syncdriven state has changed
if (seriesIndexesChanged || hoverStateChanged || cursorDrivenBySyncChanged) {
ctx.scheduleRender();
}
};
}
export function createSetSeriesHandler(
ctx: TooltipControllerContext,
syncTooltipWithDashboard: boolean,
): (u: uPlot, seriesIdx: number | null, opts: uPlot.Series) => void {
return (u: uPlot, seriesIdx: number | null, opts: uPlot.Series): void => {
const { controller } = ctx;
if (!('focus' in opts)) {
return;
}
// Remember which series is focused so we can drive hover
// logic even when the tooltip is being synced externally.
controller.focusedSeriesIndex = seriesIdx ?? null;
controller.cursorDrivenBySync = u.cursor.event == null;
updateHoverState(controller, syncTooltipWithDashboard);
ctx.scheduleRender();
};
}

View File

@@ -1,91 +0,0 @@
import { CSSProperties } from 'react';
import { TooltipRenderArgs } from '../../components/types';
import { UPlotConfigBuilder } from '../../config/UPlotConfigBuilder';
export const TOOLTIP_OFFSET = 10;
export enum DashboardCursorSync {
Crosshair,
None,
Tooltip,
}
export interface TooltipViewState {
plot?: uPlot | null;
style: Partial<CSSProperties>;
isHovering: boolean;
isPinned: boolean;
dismiss: () => void;
contents?: React.ReactNode;
}
export interface TooltipLayoutInfo {
observer: ResizeObserver;
width: number;
height: number;
}
export interface TooltipPluginProps {
config: UPlotConfigBuilder;
canPinTooltip?: boolean;
syncMode?: DashboardCursorSync;
syncKey?: string;
render: (args: TooltipRenderArgs) => React.ReactNode;
maxWidth?: number;
maxHeight?: number;
}
/**
* Mutable, non-React state that drives tooltip behaviour:
* - whether the tooltip is active / pinned
* - where it should be positioned
* - which series / data indexes are active
*
* This state lives outside of React so that uPlot hooks and DOM
* event handlers can update it freely without causing rerenders
* on every tiny interaction. React is only updated when a render
* is explicitly scheduled from the plugin.
*/
export interface TooltipControllerState {
plot: uPlot | null;
hoverActive: boolean;
isAnySeriesActive: boolean;
pinned: boolean;
style: TooltipViewState['style'];
horizontalOffset: number;
verticalOffset: number;
seriesIndexes: Array<number | null>;
focusedSeriesIndex: number | null;
cursorDrivenBySync: boolean;
plotWithinViewport: boolean;
windowWidth: number;
windowHeight: number;
pendingPinnedUpdate: boolean;
}
/**
* Context passed to uPlot hook handlers.
*
* It gives the handlers access to:
* - the shared controller state
* - layout / container refs
* - the React `updateState` function
* - render & dismiss helpers from the plugin
*/
export interface TooltipControllerContext {
controller: TooltipControllerState;
layoutRef: React.MutableRefObject<TooltipLayoutInfo | undefined>;
containerRef: React.RefObject<HTMLDivElement | null>;
rafId: React.MutableRefObject<number | null>;
updateState: (updates: Partial<TooltipViewState>) => void;
renderRef: React.MutableRefObject<
(args: TooltipRenderArgs) => React.ReactNode
>;
syncMode: DashboardCursorSync;
syncKey: string;
canPinTooltip: boolean;
createTooltipContents: () => React.ReactNode;
scheduleRender: (updatePinned?: boolean) => void;
dismissTooltip: () => void;
}

View File

@@ -1,159 +0,0 @@
import { TOOLTIP_OFFSET, TooltipLayoutInfo, TooltipViewState } from './types';
export function isPlotInViewport(
rect: uPlot.BBox,
windowWidth: number,
windowHeight: number,
): boolean {
return (
rect.top + rect.height <= windowHeight &&
rect.top >= 0 &&
rect.left >= 0 &&
rect.left + rect.width <= windowWidth
);
}
export function calculateVerticalOffset(
currentOffset: number,
clientY: number,
tooltipHeight: number,
windowHeight: number,
): number {
const height = tooltipHeight + TOOLTIP_OFFSET;
if (currentOffset !== 0) {
if (clientY + height < windowHeight || clientY - height < 0) {
return 0;
}
if (currentOffset !== -height) {
return -height;
}
return currentOffset;
}
if (clientY + height > windowHeight && clientY - height >= 0) {
return -height;
}
return 0;
}
export function calculateHorizontalOffset(
currentOffset: number,
clientX: number,
tooltipWidth: number,
windowWidth: number,
): number {
const width = tooltipWidth + TOOLTIP_OFFSET;
if (currentOffset !== 0) {
if (clientX + width < windowWidth || clientX - width < 0) {
return 0;
}
if (currentOffset !== -width) {
return -width;
}
return currentOffset;
}
if (clientX + width > windowWidth && clientX - width >= 0) {
return -width;
}
return 0;
}
export function calculateTooltipOffset(
clientX: number,
clientY: number,
tooltipWidth: number,
tooltipHeight: number,
currentHorizontalOffset: number,
currentVerticalOffset: number,
windowWidth: number,
windowHeight: number,
): { horizontalOffset: number; verticalOffset: number } {
return {
horizontalOffset: calculateHorizontalOffset(
currentHorizontalOffset,
clientX,
tooltipWidth,
windowWidth,
),
verticalOffset: calculateVerticalOffset(
currentVerticalOffset,
clientY,
tooltipHeight,
windowHeight,
),
};
}
export function buildTransform(
clientX: number,
clientY: number,
hOffset: number,
vOffset: number,
): string {
const translateX =
clientX + (hOffset === 0 ? TOOLTIP_OFFSET : -TOOLTIP_OFFSET);
const translateY =
clientY + (vOffset === 0 ? TOOLTIP_OFFSET : -TOOLTIP_OFFSET);
const reflectX = hOffset === 0 ? '' : 'translateX(-100%)';
const reflectY = vOffset === 0 ? '' : 'translateY(-100%)';
return `translateX(${translateX}px) ${reflectX} translateY(${translateY}px) ${reflectY}`;
}
/**
* React view state for the tooltip.
*
* This is the minimal data needed to render:
* - current position / CSS style
* - whether the tooltip is visible or pinned
* - the React node to show as contents
* - the associated uPlot instance (for children)
*
* All interaction logic lives in the controller; that logic calls
* `updateState` to push the latest snapshot into React.
*/
export function createInitialViewState(): TooltipViewState {
return {
style: { transform: '', pointerEvents: 'none' },
isHovering: false,
isPinned: false,
contents: null,
plot: null,
dismiss: (): void => {},
};
}
/**
* Creates and wires a ResizeObserver that keeps track of the rendered
* tooltip size. This is used by the controller to place the tooltip
* on the correct side of the cursor and avoid clipping the viewport.
*/
export function createLayoutObserver(
layoutRef: React.MutableRefObject<TooltipLayoutInfo | undefined>,
): TooltipLayoutInfo {
const layout: TooltipLayoutInfo = {
width: 0,
height: 0,
observer: new ResizeObserver((entries) => {
const current = layoutRef.current;
if (!current) {
return;
}
for (const entry of entries) {
if (entry.borderBoxSize?.length) {
current.width = entry.borderBoxSize[0].inlineSize;
current.height = entry.borderBoxSize[0].blockSize;
} else {
current.width = entry.contentRect.width;
current.height = entry.contentRect.height;
}
}
}),
};
return layout;
}

View File

@@ -1,11 +0,0 @@
export function resolveSeriesVisibility(
label: string,
seriesShow: boolean | undefined | null,
visibilityMap: Map<string, boolean> | null,
isAnySeriesHidden: boolean,
): boolean {
if (isAnySeriesHidden) {
return visibilityMap?.get(label) ?? false;
}
return seriesShow ?? true;
}

View File

@@ -1,17 +1,15 @@
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { Breadcrumb, Button, Divider } from 'antd';
import { Breadcrumb, Button, Divider, Empty } from 'antd';
import logEvent from 'api/common/logEvent';
import classNames from 'classnames';
import { Filters } from 'components/AlertDetailsFilters/Filters';
import RouteTab from 'components/RouteTab';
import Spinner from 'components/Spinner';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { CreateAlertProvider } from 'container/CreateAlertV2/context';
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import {
@@ -20,7 +18,6 @@ import {
} from 'types/api/alerts/alertTypesV2';
import AlertHeader from './AlertHeader/AlertHeader';
import AlertNotFound from './AlertNotFound';
import { useGetAlertRuleDetails, useRouteTabUtils } from './hooks';
import { AlertDetailsStatusRendererProps } from './types';
@@ -80,7 +77,7 @@ BreadCrumbItem.defaultProps = {
function AlertDetails(): JSX.Element {
const { pathname } = useLocation();
const { routes } = useRouteTabUtils();
const params = useUrlQuery();
const { t } = useTranslation(['alerts']);
const {
isLoading,
@@ -91,27 +88,10 @@ function AlertDetails(): JSX.Element {
alertDetailsResponse,
} = useGetAlertRuleDetails();
const isTestAlert = useMemo(() => {
return params.get(QueryParams.isTestAlert) === 'true';
}, [params]);
const getDocumentTitle = useMemo(() => {
const alertTitle = alertDetailsResponse?.payload?.data?.alert;
if (alertTitle) {
return alertTitle;
}
if (isTestAlert) {
return 'Test Alert';
}
if (isLoading) {
return document.title;
}
return 'Alert Not Found';
}, [alertDetailsResponse?.payload?.data?.alert, isTestAlert, isLoading]);
useEffect(() => {
document.title = getDocumentTitle;
}, [getDocumentTitle]);
const alertTitle = alertDetailsResponse?.payload?.data.alert;
document.title = alertTitle || document.title;
}, [alertDetailsResponse?.payload?.data.alert, isRefetching]);
const alertRuleDetails = useMemo(
() => alertDetailsResponse?.payload?.data as PostableAlertRuleV2 | undefined,
@@ -126,10 +106,13 @@ function AlertDetails(): JSX.Element {
if (
isError ||
!isValidRuleId ||
(alertDetailsResponse && alertDetailsResponse.statusCode !== 200) ||
(!isLoading && !alertRuleDetails)
(alertDetailsResponse && alertDetailsResponse.statusCode !== 200)
) {
return <AlertNotFound isTestAlert={isTestAlert} />;
return (
<div className="alert-empty-card">
<Empty description={t('alert_rule_not_found')} />
</div>
);
}
const handleTabChange = (route: string): void => {

View File

@@ -1,136 +0,0 @@
.alert-not-found {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 60vh;
width: 500px;
gap: 24px;
margin: 0 auto;
.description {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
.not-found-img {
height: 32px;
width: 32px;
}
.not-found-text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
text-align: center;
}
}
.reasons {
display: flex;
flex-direction: column;
gap: 12px;
.reason {
display: flex;
padding: 12px;
align-items: flex-start;
gap: 12px;
border-radius: 4px;
background: rgba(171, 189, 255, 0.04);
.construction-img,
.broom-img {
height: 16px;
width: 16px;
}
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
}
}
.none-of-above {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
text-align: center;
}
.action-btns {
display: flex;
gap: 8px;
.action-btn {
display: flex;
width: 160px;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 8px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
box-shadow: none;
.ant-btn-icon {
margin-inline-end: 0px;
}
}
}
}
}
.lightMode {
.alert-not-found {
.description {
.not-found-text {
color: var(--bg-ink-400);
}
}
.reasons {
.reason {
background: var(--bg-vanilla-300);
.text {
color: var(--bg-ink-400);
}
}
}
.none-of-above {
.text {
color: var(--bg-ink-400);
}
.action-btns {
.action-btn {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-300);
}
}
}
}
}

View File

@@ -1,96 +0,0 @@
import { Button, Typography } from 'antd';
import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { LifeBuoy, List } from 'lucide-react';
import { handleContactSupport } from 'pages/Integrations/utils';
import './AlertNotFound.styles.scss';
interface AlertNotFoundProps {
isTestAlert: boolean;
}
function AlertNotFound({ isTestAlert }: AlertNotFoundProps): JSX.Element {
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const { safeNavigate } = useSafeNavigate();
const checkAllRulesHandler = (): void => {
safeNavigate(ROUTES.LIST_ALL_ALERT);
};
const contactSupportHandler = (): void => {
handleContactSupport(isCloudUserVal);
};
return (
<div className="alert-not-found">
<section className="description">
<img src="/Icons/no-data.svg" alt="no-data" className="not-found-img" />
<Typography.Text className="not-found-text">
Uh-oh! We couldn&apos;t find the given alert rule.
</Typography.Text>
<Typography.Text className="not-found-text">
{isTestAlert
? 'This can happen in the following scenario -'
: 'This can happen in either of the following scenarios -'}
</Typography.Text>
</section>
<section className="reasons">
{!isTestAlert && (
<>
<div className="reason">
<img
src="/Icons/construction.svg"
alt="no-data"
className="construction-img"
/>
<Typography.Text className="text">
The alert rule link is incorrect, please verify it once.
</Typography.Text>
</div>
<div className="reason">
<img src="/Icons/broom.svg" alt="no-data" className="broom-img" />
<Typography.Text className="text">
The alert rule you&apos;re trying to check has been deleted.
</Typography.Text>
</div>
</>
)}
{isTestAlert && (
<div className="reason">
<img src="/Icons/broom.svg" alt="no-data" className="broom-img" />
<Typography.Text className="text">
You clicked on the Alert notification link received when testing a new
Alert rule. Once the alert rule is saved, future notifications will link
to actual alerts.
</Typography.Text>
</div>
)}
</section>
<section className="none-of-above">
<Typography.Text className="text">
If you feel the issue is none of the above, please contact support.
</Typography.Text>
<div className="action-btns">
<Button
className="action-btn"
icon={<List size={14} />}
onClick={checkAllRulesHandler}
>
Check all rules
</Button>
<Button
className="action-btn"
icon={<LifeBuoy size={14} />}
onClick={contactSupportHandler}
>
Contact Support
</Button>
</div>
</section>
</div>
);
}
export default AlertNotFound;

View File

@@ -1,3 +0,0 @@
import AlertNotFound from './AlertNotFound';
export default AlertNotFound;

View File

@@ -1,92 +0,0 @@
import { render, screen } from '@testing-library/react';
import ROUTES from 'constants/routes';
import * as useGetTenantLicense from 'hooks/useGetTenantLicense';
import * as useSafeNavigate from 'hooks/useSafeNavigate';
import { userEvent } from 'tests/test-utils';
import AlertNotFound from '../AlertNotFound';
jest.mock('lib/history', () => ({
__esModule: true,
default: {
push: jest.fn(),
},
}));
import history from 'lib/history';
const mockSafeNavigate = jest.fn();
const useGetTenantLicenseSpy = jest.spyOn(
useGetTenantLicense,
'useGetTenantLicense',
);
const useSafeNavigateSpy = jest.spyOn(useSafeNavigate, 'useSafeNavigate');
describe('AlertNotFound', () => {
beforeEach(() => {
mockSafeNavigate.mockClear();
window.open = jest.fn();
useGetTenantLicenseSpy.mockReturnValue({
isCloudUser: false,
} as ReturnType<typeof useGetTenantLicense.useGetTenantLicense>);
useSafeNavigateSpy.mockReturnValue({
safeNavigate: mockSafeNavigate,
});
});
it('should render the correct error message for test alerts', () => {
render(<AlertNotFound isTestAlert />);
expect(
screen.getByText("Uh-oh! We couldn't find the given alert rule."),
).toBeInTheDocument();
expect(
screen.getByText('This can happen in the following scenario -'),
).toBeInTheDocument();
expect(
screen.getByText(
'You clicked on the Alert notification link received when testing a new Alert rule. Once the alert rule is saved, future notifications will link to actual alerts.',
),
).toBeInTheDocument();
});
it('should render the correct error message for non-existing alerts', () => {
render(<AlertNotFound isTestAlert={false} />);
expect(
screen.getByText("Uh-oh! We couldn't find the given alert rule."),
).toBeInTheDocument();
expect(
screen.getByText('This can happen in either of the following scenarios -'),
).toBeInTheDocument();
expect(
screen.getByText('The alert rule link is incorrect, please verify it once.'),
).toBeInTheDocument();
expect(
screen.getByText("The alert rule you're trying to check has been deleted."),
).toBeInTheDocument();
});
it('should navigate to the list all alerts page when the check all rules button is clicked', async () => {
const user = userEvent.setup();
render(<AlertNotFound isTestAlert={false} />);
await user.click(screen.getByText('Check all rules'));
expect(mockSafeNavigate).toHaveBeenCalledWith(ROUTES.LIST_ALL_ALERT);
});
it('should navigate to the correct support page for cloud users when button is clicked', async () => {
const user = userEvent.setup();
useGetTenantLicenseSpy.mockReturnValueOnce({
isCloudUser: true,
} as ReturnType<typeof useGetTenantLicense.useGetTenantLicense>);
render(<AlertNotFound isTestAlert={false} />);
await user.click(screen.getByText('Contact Support'));
expect(history.push).toHaveBeenCalledWith('/support');
});
it('should navigate to the support page for self-hosted users when the contact support button is clicked', async () => {
const user = userEvent.setup();
render(<AlertNotFound isTestAlert={false} />);
await user.click(screen.getByText('Contact Support'));
expect(window.open).toHaveBeenCalledWith('https://signoz.io/slack', '_blank');
});
});

View File

@@ -33,19 +33,6 @@
height: calc(100vh - 48px);
border-right: 1px solid var(--Slate-500, #161922);
background: var(--Ink-500, #0b0c0e);
margin-top: 4px;
.nav-item {
.nav-item-data {
margin: 0px 8px 0px 4px;
}
&.active {
.nav-item-data .nav-item-label {
color: var(--bg-vanilla-100, #fff);
}
}
}
}
.settings-page-content {
@@ -94,14 +81,6 @@
.settings-page-sidenav {
border-right: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.nav-item {
&.active {
.nav-item-data .nav-item-label {
color: var(--bg-ink-500);
}
}
}
}
.settings-page-content {

View File

@@ -13,7 +13,7 @@ import { SidebarItem } from 'container/SideNav/sideNav.types';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
import { Cog } from 'lucide-react';
import { Wrench } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
@@ -236,7 +236,7 @@ function SettingsPage(): JSX.Element {
className="settings-page-header-title"
data-testid="settings-page-title"
>
<Cog size={16} />
<Wrench size={16} />
Settings
</div>
</header>

View File

@@ -30,7 +30,7 @@
font-weight: 600;
line-height: 1;
letter-spacing: 0;
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #eceef2);
margin: 0 !important;
}
@@ -40,7 +40,7 @@
font-weight: 400;
line-height: 20px;
letter-spacing: -0.065px;
color: var(--l2-foreground);
color: var(--semantic-secondary-foreground, #adb4c2);
max-width: 360px;
margin: 0 !important;
text-align: center;
@@ -52,8 +52,8 @@
.signup-form-container {
width: 100%;
background: var(--bg-neutral-dark-950);
border: 1px solid var(--l3-background);
background: var(--semantic-secondary-background, #121317);
border: 1px solid var(--semantic-secondary-border, #23262e);
border-radius: 4px;
padding: 24px;
@@ -81,8 +81,8 @@
&.ant-input-affix-wrapper {
height: 32px;
border-radius: 2px;
background: var(--l3-background);
border-color: var(--l3-border);
background: var(--levels-l3-background, #23262e);
border-color: var(--levels-l3-border, #2c303a);
}
&.ant-input-affix-wrapper {
@@ -95,8 +95,8 @@
.signup-form-input {
height: 32px;
background: var(--l3-background);
border: 1px solid var(--l3-border);
background: var(--levels-l3-background, #23262e);
border: 1px solid var(--levels-l3-border, #2c303a);
border-radius: 2px;
padding: 6px 8px;
font-family: Inter, sans-serif;
@@ -104,18 +104,18 @@
font-weight: 400;
line-height: 1;
letter-spacing: -0.065px;
color: var(--l1-foreground);
color: var(--levels-l1-foreground, #eceef2);
&::placeholder {
color: var(--l3-foreground);
color: var(--levels-l3-foreground, #747b8b);
}
&:hover {
border-color: var(--l3-border);
border-color: var(--levels-l3-border, #2c303a);
}
&:focus {
border-color: var(--primary);
border-color: var(--semantic-primary-background, #4e74f8);
box-shadow: none;
}
}
@@ -134,7 +134,7 @@
}
.signup-info-message {
color: var(--l2-foreground);
color: var(--semantic-secondary-foreground);
font-size: 11px;
font-weight: 400;
line-height: 1.45;
@@ -178,51 +178,51 @@
}
.signup-header-subtitle {
color: var(--text-neutral-light-200);
color: var(--text-neutral-light-200, #80828d);
}
}
.signup-form {
.signup-form-container {
background: var(--bg-base-white);
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-base-white, #ffffff);
border: 1px solid var(--bg-vanilla-300, #e9e9e9);
.signup-form-input,
.signup-antd-input {
&.ant-input,
&.ant-input-password,
&.ant-input-affix-wrapper {
background: var(--bg-vanilla-200);
border-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-200, #f5f5f5);
border-color: var(--bg-vanilla-300, #e9e9e9);
color: var(--text-ink-500);
}
&::placeholder {
color: var(--text-neutral-light-200);
color: var(--text-neutral-light-200, #80828d);
}
&:focus {
border-color: var(--primary);
border-color: var(--semantic-primary-background, #4e74f8);
}
}
.signup-form-input {
background: var(--bg-vanilla-200);
border-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-200, #f5f5f5);
border-color: var(--bg-vanilla-300, #e9e9e9);
color: var(--text-ink-500);
&::placeholder {
color: var(--text-neutral-light-200);
color: var(--text-neutral-light-200, #80828d);
}
&:focus {
border-color: var(--primary);
border-color: var(--semantic-primary-background, #4e74f8);
}
}
}
.signup-info-message {
color: var(--text-neutral-light-200);
color: var(--text-neutral-light-200, #80828d);
}
}
}

Some files were not shown because too many files have changed in this diff Show More