mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-20 18:30:41 +01:00
Compare commits
16 Commits
nv/area-ch
...
refactor/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69e6a54569 | ||
|
|
ea8f95ee08 | ||
|
|
b64116d67d | ||
|
|
6a9448ae5f | ||
|
|
a3aa45d31b | ||
|
|
9dcccf24dc | ||
|
|
af7d4b5c58 | ||
|
|
66260542ca | ||
|
|
9d410b3c41 | ||
|
|
818e4f4476 | ||
|
|
1ccdb198cd | ||
|
|
06d4e61d8d | ||
|
|
b26eb46bdf | ||
|
|
fe8482e62c | ||
|
|
2068482f66 | ||
|
|
35973efd65 |
@@ -171,6 +171,14 @@ components:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelDefect:
|
||||
enum:
|
||||
- none
|
||||
- missing_type
|
||||
- multiple_notifiers
|
||||
- unsupported_notifier
|
||||
- unrepresentable
|
||||
type: string
|
||||
AlertmanagertypesChannelEmailConfig:
|
||||
properties:
|
||||
headers:
|
||||
@@ -384,6 +392,40 @@ components:
|
||||
required:
|
||||
- routingKey
|
||||
type: object
|
||||
AlertmanagertypesChannelRepair:
|
||||
properties:
|
||||
action:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelRepairAction'
|
||||
applied:
|
||||
type: boolean
|
||||
blockers:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
channels:
|
||||
items:
|
||||
$ref: '#/components/schemas/AlertmanagertypesListedNotificationChannel'
|
||||
nullable: true
|
||||
type: array
|
||||
defect:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelDefect'
|
||||
detail:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- defect
|
||||
- action
|
||||
- applied
|
||||
type: object
|
||||
AlertmanagertypesChannelRepairAction:
|
||||
enum:
|
||||
- none
|
||||
- retype
|
||||
- split
|
||||
- delete
|
||||
type: string
|
||||
AlertmanagertypesChannelSlackAction:
|
||||
properties:
|
||||
confirm:
|
||||
@@ -1033,6 +1075,11 @@ components:
|
||||
- duration
|
||||
- repeatType
|
||||
type: object
|
||||
AlertmanagertypesRepairChannelParams:
|
||||
properties:
|
||||
apply:
|
||||
type: boolean
|
||||
type: object
|
||||
AlertmanagertypesRepeatOn:
|
||||
enum:
|
||||
- sunday
|
||||
@@ -9685,6 +9732,8 @@ components:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
origin:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
@@ -9697,6 +9746,7 @@ components:
|
||||
- fieldContext
|
||||
- config
|
||||
- enabled
|
||||
- origin
|
||||
type: object
|
||||
SpantypesSpanMapperConfig:
|
||||
properties:
|
||||
@@ -9725,48 +9775,75 @@ components:
|
||||
type: string
|
||||
orgId:
|
||||
type: string
|
||||
origin:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
version:
|
||||
type: integer
|
||||
required:
|
||||
- id
|
||||
- orgId
|
||||
- name
|
||||
- condition
|
||||
- enabled
|
||||
- origin
|
||||
- version
|
||||
type: object
|
||||
SpantypesSpanMapperGroupCondition:
|
||||
nullable: true
|
||||
properties:
|
||||
attributes:
|
||||
items:
|
||||
type: string
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperGroupConditionKey'
|
||||
nullable: true
|
||||
type: array
|
||||
resource:
|
||||
items:
|
||||
type: string
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperGroupConditionKey'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- attributes
|
||||
- resource
|
||||
type: object
|
||||
SpantypesSpanMapperGroupConditionKey:
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
origin:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
|
||||
value:
|
||||
type: string
|
||||
required:
|
||||
- value
|
||||
- enabled
|
||||
type: object
|
||||
SpantypesSpanMapperOperation:
|
||||
enum:
|
||||
- move
|
||||
- copy
|
||||
type: string
|
||||
SpantypesSpanMapperOrigin:
|
||||
enum:
|
||||
- user
|
||||
- system
|
||||
type: string
|
||||
SpantypesSpanMapperSource:
|
||||
properties:
|
||||
context:
|
||||
$ref: '#/components/schemas/SpantypesFieldContext'
|
||||
enabled:
|
||||
type: boolean
|
||||
key:
|
||||
type: string
|
||||
operation:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperOperation'
|
||||
origin:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
|
||||
priority:
|
||||
type: integer
|
||||
required:
|
||||
@@ -9774,6 +9851,7 @@ components:
|
||||
- context
|
||||
- operation
|
||||
- priority
|
||||
- enabled
|
||||
type: object
|
||||
SpantypesSpanMapperTestSpan:
|
||||
properties:
|
||||
@@ -20306,6 +20384,85 @@ paths:
|
||||
summary: Update notification channel
|
||||
tags:
|
||||
- channels
|
||||
/api/v2/notification_channels/{id}/repair:
|
||||
post:
|
||||
deprecated: false
|
||||
description: 'This endpoint diagnoses a stored channel that the v2 API cannot
|
||||
read and applies the fitting action: a channel carrying several notifier configurations
|
||||
is split into one channel per configuration, keeping this ID for the first;
|
||||
a channel whose notifier kind v2 does not model is deleted; a channel with
|
||||
an empty stored type has it rewritten from its data. A delete is refused while
|
||||
a routing policy still names the channel. Nothing is written unless apply=true;
|
||||
by default the response only shows what would happen.'
|
||||
operationId: RepairNotificationChannel
|
||||
parameters:
|
||||
- in: query
|
||||
name: apply
|
||||
schema:
|
||||
type: boolean
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlertmanagertypesRepairChannelParams'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelRepair'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- notification-channel:update
|
||||
- tokenizer:
|
||||
- notification-channel:update
|
||||
summary: Repair notification channel
|
||||
tags:
|
||||
- channels
|
||||
/api/v2/notification_channels/test:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"GET_STARTED": "SigNoz | Get Started",
|
||||
"SERVICE_METRICS": "SigNoz | Service Metrics",
|
||||
"SERVICE_MAP": "SigNoz | Service Map",
|
||||
"TRACE": "SigNoz | Trace",
|
||||
"HOME": "SigNoz | Home",
|
||||
"TRACE_DETAIL": "SigNoz | Trace Detail",
|
||||
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
|
||||
@@ -56,7 +55,6 @@
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
|
||||
"MCP_SERVER": "SigNoz | MCP Server",
|
||||
"AI_ASSISTANT": "SigNoz | AI Assistant",
|
||||
"TRACE_DETAIL_OLD": "SigNoz | Trace Detail",
|
||||
"SERVICE_TOP_LEVEL_OPERATIONS": "SigNoz | Service Operations",
|
||||
"ROLE_DETAILS": "SigNoz | Role Details",
|
||||
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"GET_STARTED_AZURE_MONITORING": "SigNoz | Get Started | AZURE",
|
||||
"GET_STARTED": "SigNoz | Get Started with SigNoz Cloud",
|
||||
"GET_STARTED_WITH_CLOUD": "SigNoz | Get Started with SigNoz Cloud",
|
||||
"TRACE": "SigNoz | Trace",
|
||||
"TRACE_DETAIL": "SigNoz | Trace Detail",
|
||||
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
|
||||
"SETTINGS": "SigNoz | Settings",
|
||||
@@ -79,7 +78,6 @@
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
|
||||
"MCP_SERVER": "SigNoz | MCP Server",
|
||||
"AI_ASSISTANT": "SigNoz | AI Assistant",
|
||||
"TRACE_DETAIL_OLD": "SigNoz | Trace Detail",
|
||||
"SERVICE_TOP_LEVEL_OPERATIONS": "SigNoz | Service Operations",
|
||||
"ROLE_DETAILS": "SigNoz | Role Details",
|
||||
"ROLE_CREATE": "SigNoz | Create Role",
|
||||
|
||||
@@ -1588,15 +1588,10 @@ describe('PrivateRoute', () => {
|
||||
deniedRoles: DENIED_ROLES,
|
||||
},
|
||||
TRACES_EXPLORER: { path: ROUTES.TRACES_EXPLORER, deniedRoles: DENIED_ROLES },
|
||||
TRACE: { path: ROUTES.TRACE, deniedRoles: DENIED_ROLES },
|
||||
TRACE_DETAIL: {
|
||||
path: ROUTES.TRACE_DETAIL.replace(':id', 'trace-id-1'),
|
||||
deniedRoles: DENIED_ROLES,
|
||||
},
|
||||
TRACE_DETAIL_OLD: {
|
||||
path: ROUTES.TRACE_DETAIL_OLD.replace(':id', 'trace-id-1'),
|
||||
deniedRoles: DENIED_ROLES,
|
||||
},
|
||||
// LOGS and LOGS_EXPLORER share a path - matchPath resolves it to whichever
|
||||
// route definition comes last, and both keys are authz-aware either way.
|
||||
LOGS: { path: ROUTES.LOGS, deniedRoles: DENIED_ROLES },
|
||||
|
||||
@@ -53,17 +53,6 @@ export const TracesFunnelDetails = Loadable(
|
||||
),
|
||||
);
|
||||
|
||||
export const TraceFilter = Loadable(
|
||||
() => import(/* webpackChunkName: "Trace Filter Page" */ 'pages/Trace'),
|
||||
);
|
||||
|
||||
export const TraceDetailOldRedirect = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "TraceDetailOldRedirect" */ 'pages/TraceDetailOldRedirect/index'
|
||||
),
|
||||
);
|
||||
|
||||
export const TraceDetailV3 = Loadable(
|
||||
() =>
|
||||
import(
|
||||
|
||||
@@ -47,9 +47,7 @@ import {
|
||||
SomethingWentWrong,
|
||||
StatusPage,
|
||||
SupportPage,
|
||||
TraceDetailOldRedirect,
|
||||
TraceDetailV3,
|
||||
TraceFilter,
|
||||
TracesExplorer,
|
||||
TracesFunnelDetails,
|
||||
TracesFunnels,
|
||||
@@ -132,14 +130,6 @@ const routes: AppRoutes[] = [
|
||||
exact: true,
|
||||
key: 'LOGS_SAVE_VIEWS',
|
||||
},
|
||||
// Legacy /trace-old/:id redirects to the current /trace/:id view.
|
||||
{
|
||||
path: ROUTES.TRACE_DETAIL_OLD,
|
||||
exact: true,
|
||||
component: TraceDetailOldRedirect,
|
||||
isPrivate: true,
|
||||
key: 'TRACE_DETAIL_OLD',
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACE_DETAIL,
|
||||
exact: true,
|
||||
@@ -224,13 +214,6 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'ALERT_OVERVIEW',
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACE,
|
||||
exact: true,
|
||||
component: TraceFilter,
|
||||
isPrivate: true,
|
||||
key: 'TRACE',
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACES_EXPLORER,
|
||||
exact: true,
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
AlertmanagertypesPostableChannelDTO,
|
||||
AlertmanagertypesPostableNotificationChannelDTO,
|
||||
AlertmanagertypesReceiverDTO,
|
||||
AlertmanagertypesRepairChannelParamsDTO,
|
||||
AlertmanagertypesTestableNotificationChannelDTO,
|
||||
AlertmanagertypesUpdatableNotificationChannelDTO,
|
||||
CreateChannel201,
|
||||
@@ -35,6 +36,9 @@ import type {
|
||||
ListNotificationChannels200,
|
||||
ListNotificationChannelsParams,
|
||||
RenderErrorResponseDTO,
|
||||
RepairNotificationChannel200,
|
||||
RepairNotificationChannelParams,
|
||||
RepairNotificationChannelPathParameters,
|
||||
UpdateChannelByIDPathParameters,
|
||||
UpdateNotificationChannel200,
|
||||
UpdateNotificationChannelPathParameters,
|
||||
@@ -1144,6 +1148,113 @@ export const useUpdateNotificationChannel = <
|
||||
> => {
|
||||
return useMutation(getUpdateNotificationChannelMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint diagnoses a stored channel that the v2 API cannot read and applies the fitting action: a channel carrying several notifier configurations is split into one channel per configuration, keeping this ID for the first; a channel whose notifier kind v2 does not model is deleted; a channel with an empty stored type has it rewritten from its data. A delete is refused while a routing policy still names the channel. Nothing is written unless apply=true; by default the response only shows what would happen.
|
||||
* @summary Repair notification channel
|
||||
*/
|
||||
export const repairNotificationChannel = (
|
||||
{ id }: RepairNotificationChannelPathParameters,
|
||||
alertmanagertypesRepairChannelParamsDTO?: BodyType<AlertmanagertypesRepairChannelParamsDTO>,
|
||||
params?: RepairNotificationChannelParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<RepairNotificationChannel200>({
|
||||
url: `/api/v2/notification_channels/${id}/repair`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: alertmanagertypesRepairChannelParamsDTO,
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getRepairNotificationChannelMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof repairNotificationChannel>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: RepairNotificationChannelPathParameters;
|
||||
data?: BodyType<AlertmanagertypesRepairChannelParamsDTO>;
|
||||
params?: RepairNotificationChannelParams;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof repairNotificationChannel>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: RepairNotificationChannelPathParameters;
|
||||
data?: BodyType<AlertmanagertypesRepairChannelParamsDTO>;
|
||||
params?: RepairNotificationChannelParams;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['repairNotificationChannel'];
|
||||
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 repairNotificationChannel>>,
|
||||
{
|
||||
pathParams: RepairNotificationChannelPathParameters;
|
||||
data?: BodyType<AlertmanagertypesRepairChannelParamsDTO>;
|
||||
params?: RepairNotificationChannelParams;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data, params } = props ?? {};
|
||||
|
||||
return repairNotificationChannel(pathParams, data, params);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type RepairNotificationChannelMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof repairNotificationChannel>>
|
||||
>;
|
||||
export type RepairNotificationChannelMutationBody =
|
||||
| BodyType<AlertmanagertypesRepairChannelParamsDTO>
|
||||
| undefined;
|
||||
export type RepairNotificationChannelMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Repair notification channel
|
||||
*/
|
||||
export const useRepairNotificationChannel = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof repairNotificationChannel>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: RepairNotificationChannelPathParameters;
|
||||
data?: BodyType<AlertmanagertypesRepairChannelParamsDTO>;
|
||||
params?: RepairNotificationChannelParams;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof repairNotificationChannel>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: RepairNotificationChannelPathParameters;
|
||||
data?: BodyType<AlertmanagertypesRepairChannelParamsDTO>;
|
||||
params?: RepairNotificationChannelParams;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getRepairNotificationChannelMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint sends a test notification for the configuration in the request body. The channel need not exist and nothing is persisted, so the body carries a configuration only.
|
||||
* @summary Test notification channel
|
||||
|
||||
@@ -596,6 +596,13 @@ export type AlertmanagertypesChannelConfigDTO =
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJSMOpsConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelIncidentIOConfigDTO;
|
||||
|
||||
export enum AlertmanagertypesChannelDefectDTO {
|
||||
none = 'none',
|
||||
missing_type = 'missing_type',
|
||||
multiple_notifiers = 'multiple_notifiers',
|
||||
unsupported_notifier = 'unsupported_notifier',
|
||||
unrepresentable = 'unrepresentable',
|
||||
}
|
||||
export enum AlertmanagertypesChannelKindDTO {
|
||||
slack = 'slack',
|
||||
email = 'email',
|
||||
@@ -617,6 +624,63 @@ export enum AlertmanagertypesChannelListSortDTO {
|
||||
created_at = 'created_at',
|
||||
name = 'name',
|
||||
}
|
||||
export enum AlertmanagertypesChannelRepairActionDTO {
|
||||
none = 'none',
|
||||
retype = 'retype',
|
||||
split = 'split',
|
||||
delete = 'delete',
|
||||
}
|
||||
export interface AlertmanagertypesListedNotificationChannelDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
displayName: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
kind: AlertmanagertypesChannelKindDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesChannelRepairDTO {
|
||||
action: AlertmanagertypesChannelRepairActionDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
applied: boolean;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
blockers?: string[];
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
channels?: AlertmanagertypesListedNotificationChannelDTO[] | null;
|
||||
defect: AlertmanagertypesChannelDefectDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
detail?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ModelLabelSetDTO {
|
||||
[key: string]: string;
|
||||
}
|
||||
@@ -1110,32 +1174,6 @@ export interface AlertmanagertypesJiraReceiverConfigDTO {
|
||||
wont_fix_resolution?: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesListedNotificationChannelDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
displayName: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
kind: AlertmanagertypesChannelKindDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesListableNotificationChannelDTO {
|
||||
/**
|
||||
* @type array
|
||||
@@ -2539,6 +2577,13 @@ export interface AlertmanagertypesReceiverDTO {
|
||||
wechat_configs?: ConfigWechatConfigDTO[];
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesRepairChannelParamsDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
apply?: boolean;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesTestableNotificationChannelDTO {
|
||||
config: AlertmanagertypesChannelConfigDTO;
|
||||
}
|
||||
@@ -10772,6 +10817,22 @@ export interface SpantypesGettableFlamegraphTraceDTO {
|
||||
startTimestampMillis: number;
|
||||
}
|
||||
|
||||
export enum SpantypesSpanMapperOriginDTO {
|
||||
user = 'user',
|
||||
system = 'system',
|
||||
}
|
||||
export interface SpantypesSpanMapperGroupConditionKeyDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled: boolean;
|
||||
origin?: SpantypesSpanMapperOriginDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
@@ -10779,11 +10840,11 @@ export type SpantypesSpanMapperGroupConditionDTO = {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
attributes: string[] | null;
|
||||
attributes: SpantypesSpanMapperGroupConditionKeyDTO[] | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
resource: string[] | null;
|
||||
resource: SpantypesSpanMapperGroupConditionKeyDTO[] | null;
|
||||
} | null;
|
||||
|
||||
export interface SpantypesSpanMapperGroupDTO {
|
||||
@@ -10813,6 +10874,7 @@ export interface SpantypesSpanMapperGroupDTO {
|
||||
* @type string
|
||||
*/
|
||||
orgId: string;
|
||||
origin: SpantypesSpanMapperOriginDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -10822,6 +10884,10 @@ export interface SpantypesSpanMapperGroupDTO {
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface SpantypesGettableSpanMapperGroupsDTO {
|
||||
@@ -10879,11 +10945,16 @@ export enum SpantypesSpanMapperOperationDTO {
|
||||
}
|
||||
export interface SpantypesSpanMapperSourceDTO {
|
||||
context: SpantypesFieldContextDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
key: string;
|
||||
operation: SpantypesSpanMapperOperationDTO;
|
||||
origin?: SpantypesSpanMapperOriginDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -10925,6 +10996,7 @@ export interface SpantypesSpanMapperDTO {
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
origin: SpantypesSpanMapperOriginDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -13484,6 +13556,25 @@ export type UpdateNotificationChannel200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type RepairNotificationChannelPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type RepairNotificationChannelParams = {
|
||||
/**
|
||||
* @type boolean
|
||||
* @description undefined
|
||||
*/
|
||||
apply?: boolean;
|
||||
};
|
||||
|
||||
export type RepairNotificationChannel200 = {
|
||||
data: AlertmanagertypesChannelRepairDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetMyOrganization200 = {
|
||||
data: TypesOrganizationDTO;
|
||||
/**
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import omitBy from 'lodash-es/omitBy';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/trace/getFilters';
|
||||
|
||||
const getFilters = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const duration =
|
||||
omitBy(props.other, (_, key) => !key.startsWith('duration')) || [];
|
||||
|
||||
const nonDuration = omitBy(props.other, (_, key) =>
|
||||
key.startsWith('duration'),
|
||||
);
|
||||
|
||||
const exclude: string[] = [];
|
||||
|
||||
props.isFilterExclude.forEach((value, key) => {
|
||||
if (value) {
|
||||
exclude.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
const response = await axios.post<PayloadProps>(`/getSpanFilters`, {
|
||||
start: props.start,
|
||||
end: props.end,
|
||||
getFilters: props.getFilters,
|
||||
...nonDuration,
|
||||
maxDuration: String((duration.duration || [])[0] || ''),
|
||||
minDuration: String((duration.duration || [])[1] || ''),
|
||||
exclude,
|
||||
spanKind: props.spanKind,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getFilters;
|
||||
@@ -1,62 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import omitBy from 'lodash-es/omitBy';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/trace/getSpans';
|
||||
|
||||
const getSpans = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
||||
Key: `${e.Key}.(string)`,
|
||||
Operator: e.Operator,
|
||||
StringValues: e.StringValues,
|
||||
NumberValues: e.NumberValues,
|
||||
BoolValues: e.BoolValues,
|
||||
}));
|
||||
|
||||
const exclude: string[] = [];
|
||||
|
||||
props.isFilterExclude.forEach((value, key) => {
|
||||
if (value) {
|
||||
exclude.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
const other = Object.fromEntries(props.selectedFilter);
|
||||
|
||||
const duration = omitBy(other, (_, key) => !key.startsWith('duration')) || [];
|
||||
|
||||
const nonDuration = omitBy(other, (_, key) => key.startsWith('duration'));
|
||||
|
||||
const response = await axios.post<PayloadProps>(
|
||||
`/getFilteredSpans/aggregates`,
|
||||
{
|
||||
start: String(props.start),
|
||||
end: String(props.end),
|
||||
function: props.function,
|
||||
groupBy: props.groupBy === 'none' ? '' : props.groupBy,
|
||||
step: props.step,
|
||||
tags: updatedSelectedTags,
|
||||
...nonDuration,
|
||||
maxDuration: String((duration.duration || [])[0] || ''),
|
||||
minDuration: String((duration.duration || [])[1] || ''),
|
||||
exclude,
|
||||
spanKind: props.spanKind,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getSpans;
|
||||
@@ -1,65 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import omitBy from 'lodash-es/omitBy';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/trace/getSpanAggregate';
|
||||
import { TraceFilterEnum } from 'types/reducer/trace';
|
||||
|
||||
const getSpanAggregate = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const preProps = {
|
||||
start: String(props.start),
|
||||
end: String(props.end),
|
||||
limit: props.limit,
|
||||
offset: props.offset,
|
||||
order: props.order,
|
||||
orderParam: props.orderParam,
|
||||
};
|
||||
|
||||
const exclude: TraceFilterEnum[] = [];
|
||||
|
||||
props.isFilterExclude.forEach((value, key) => {
|
||||
if (value) {
|
||||
exclude.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
||||
Key: `${e.Key}.(string)`,
|
||||
Operator: e.Operator,
|
||||
StringValues: e.StringValues,
|
||||
NumberValues: e.NumberValues,
|
||||
BoolValues: e.BoolValues,
|
||||
}));
|
||||
|
||||
const other = Object.fromEntries(props.selectedFilter);
|
||||
|
||||
const duration = omitBy(other, (_, key) => !key.startsWith('duration')) || [];
|
||||
|
||||
const nonDuration = omitBy(other, (_, key) => key.startsWith('duration'));
|
||||
|
||||
const response = await axios.post<PayloadProps>(`/getFilteredSpans`, {
|
||||
...preProps,
|
||||
tags: updatedSelectedTags,
|
||||
...nonDuration,
|
||||
maxDuration: String((duration.duration || [])[0] || ''),
|
||||
minDuration: String((duration.duration || [])[1] || ''),
|
||||
exclude,
|
||||
spanKind: props.spanKind,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getSpanAggregate;
|
||||
@@ -1,49 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { omitBy } from 'lodash-es';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/trace/getTagFilters';
|
||||
import { TraceFilterEnum } from 'types/reducer/trace';
|
||||
|
||||
const getTagFilters = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const duration =
|
||||
omitBy(props.other, (_, key) => !key.startsWith('duration')) || [];
|
||||
|
||||
const exclude: TraceFilterEnum[] = [];
|
||||
|
||||
props.isFilterExclude.forEach((value, key) => {
|
||||
if (value) {
|
||||
exclude.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
const nonDuration = omitBy(props.other, (_, key) =>
|
||||
key.startsWith('duration'),
|
||||
);
|
||||
|
||||
const response = await axios.post<PayloadProps>(`/getTagFilters`, {
|
||||
start: String(props.start),
|
||||
end: String(props.end),
|
||||
...nonDuration,
|
||||
maxDuration: String((duration.duration || [])[0] || ''),
|
||||
minDuration: String((duration.duration || [])[1] || ''),
|
||||
exclude,
|
||||
spanKind: props.spanKind,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getTagFilters;
|
||||
@@ -1,31 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/trace/getTagValue';
|
||||
|
||||
const getTagValue = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>(`/getTagValues`, {
|
||||
start: props.start.toString(),
|
||||
end: props.end.toString(),
|
||||
tagKey: {
|
||||
Key: props.tagKey.Key,
|
||||
Type: props.tagKey.Type,
|
||||
},
|
||||
spanKind: props.spanKind,
|
||||
});
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getTagValue;
|
||||
@@ -15,7 +15,7 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import CheckboxFilterHeader from './CheckboxFilterHeader';
|
||||
import CheckboxValueRow from './CheckboxValueRow';
|
||||
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
|
||||
import useActiveQueryIndex from './useActiveQueryIndex';
|
||||
import useActiveQueryIndex from 'components/QuickFilters/hooks/useActiveQueryIndex';
|
||||
import useCheckboxDisclosure from './useCheckboxDisclosure';
|
||||
import useCheckboxFilterActions from './useCheckboxFilterActions';
|
||||
import useCheckboxFilterState from './useCheckboxFilterState';
|
||||
|
||||
@@ -56,6 +56,57 @@ export function mockFieldsValuesAPI(response: {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records every request the AI observability values endpoint receives, so a test
|
||||
* can assert both the routing and the query params it was called with.
|
||||
*/
|
||||
export function mockAIObservabilityFieldsValuesAPI(response: {
|
||||
relatedValues?: (string | null)[];
|
||||
stringValues?: (string | null)[];
|
||||
numberValues?: (number | null)[];
|
||||
}): { requests: URLSearchParams[] } {
|
||||
const requests: URLSearchParams[] = [];
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
'http://localhost/api/v1/ai_observability/fields/values',
|
||||
(req, res, ctx) => {
|
||||
requests.push(req.url.searchParams);
|
||||
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
values: {
|
||||
relatedValues: response.relatedValues ?? [],
|
||||
stringValues: response.stringValues ?? [],
|
||||
numberValues: response.numberValues ?? [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return { requests };
|
||||
}
|
||||
|
||||
/** Fails the test if the signal-wide values endpoint is hit at all. */
|
||||
export function forbidFieldsValuesAPI(): { called: boolean } {
|
||||
const state = { called: false };
|
||||
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/fields/values', (_, res, ctx) => {
|
||||
state.called = true;
|
||||
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
|
||||
}),
|
||||
);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export function mockFieldsValuesAPILoading(): void {
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/fields/values', (_, res, ctx) =>
|
||||
|
||||
@@ -16,7 +16,7 @@ import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { NON_SELECTED_OPERATORS } from '../checkboxFilterQuery';
|
||||
import useActiveQueryIndex from '../useActiveQueryIndex';
|
||||
import useActiveQueryIndex from 'components/QuickFilters/hooks/useActiveQueryIndex';
|
||||
import useCheckboxDisclosure from '../useCheckboxDisclosure';
|
||||
import useCheckboxFilterActions from '../useCheckboxFilterActions';
|
||||
import useCheckboxFilterState from '../useCheckboxFilterState';
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { render } from 'tests/test-utils';
|
||||
|
||||
import { QuickFiltersSource } from '../../../../types';
|
||||
|
||||
import CheckboxFilterV2 from '../CheckboxFilterV2';
|
||||
import {
|
||||
DEFAULT_FILTER,
|
||||
DEFAULT_USE_FIELD_APIS,
|
||||
forbidFieldsValuesAPI,
|
||||
mockAIObservabilityFieldsValuesAPI,
|
||||
mockFieldsValuesAPI,
|
||||
setupServer,
|
||||
} from '../CheckboxFilterV2.testUtils';
|
||||
|
||||
setupServer();
|
||||
|
||||
describe('CheckboxFilterV2 - AI observability routing', () => {
|
||||
it('reads values from the AI observability endpoint and never the signal-wide one', async () => {
|
||||
const aiEndpoint = mockAIObservabilityFieldsValuesAPI({
|
||||
stringValues: ['openai', 'anthropic'],
|
||||
});
|
||||
const fieldsEndpoint = forbidFieldsValuesAPI();
|
||||
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(screen.findByText('openai')).resolves.toBeInTheDocument();
|
||||
expect(screen.getByText('anthropic')).toBeInTheDocument();
|
||||
expect(fieldsEndpoint.called).toBe(false);
|
||||
expect(aiEndpoint.requests).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('forwards the filter key and the time range to the AI observability endpoint', async () => {
|
||||
const aiEndpoint = mockAIObservabilityFieldsValuesAPI({
|
||||
stringValues: ['openai'],
|
||||
});
|
||||
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText('openai');
|
||||
|
||||
const params = aiEndpoint.requests[0];
|
||||
expect(params.get('name')).toBe(DEFAULT_FILTER.attributeKey.key);
|
||||
expect(params.get('startUnixMilli')).toBe(
|
||||
String(DEFAULT_USE_FIELD_APIS.startUnixMilli),
|
||||
);
|
||||
expect(params.get('endUnixMilli')).toBe(
|
||||
String(DEFAULT_USE_FIELD_APIS.endUnixMilli),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps non-AI sources on the signal-wide endpoint', async () => {
|
||||
mockFieldsValuesAPI({ stringValues: ['production'] });
|
||||
const aiEndpoint = mockAIObservabilityFieldsValuesAPI({
|
||||
stringValues: ['should-not-be-used'],
|
||||
});
|
||||
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(screen.findByText('production')).resolves.toBeInTheDocument();
|
||||
await waitFor(() => expect(aiEndpoint.requests).toHaveLength(0));
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useGetFieldsValues } from 'api/generated/services/fields';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { FieldValuesConfig } from 'api/querySuggestions/types';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { FIELD_API_CACHE_TIME } from 'constants/queryCacheTime';
|
||||
import { useFieldValuesSuggestion } from 'hooks/querySuggestions/useFieldValuesSuggestion';
|
||||
import { BuilderQueryType } from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL } from 'types/common/queryBuilder';
|
||||
|
||||
interface UseFieldValuesProps {
|
||||
@@ -42,32 +43,43 @@ export function useFieldValues({
|
||||
endUnixMilli,
|
||||
enabled,
|
||||
}: UseFieldValuesProps): UseFieldValuesReturn {
|
||||
const { data, isLoading, isFetching } = useGetFieldsValues(
|
||||
{
|
||||
signal: filter.dataSource
|
||||
? DATA_SOURCE_TO_SIGNAL[filter.dataSource]
|
||||
: undefined,
|
||||
name: filter.attributeKey.key,
|
||||
searchText,
|
||||
existingQuery,
|
||||
metricNamespace,
|
||||
source: source ? QUICK_FILTERS_SOURCE_TO_SOURCE[source] : undefined,
|
||||
startUnixMilli,
|
||||
// This field does not affect the backend but I wanted to keep it here
|
||||
// in case we add the support in the future
|
||||
endUnixMilli,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
enabled,
|
||||
cacheTime: FIELD_API_CACHE_TIME,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
const isAIObservability = source === QuickFiltersSource.AI_OBSERVABILITY;
|
||||
|
||||
const builderQueryType: BuilderQueryType | undefined = isAIObservability
|
||||
? 'builder_ai_query'
|
||||
: undefined;
|
||||
|
||||
// The AI values endpoint is already gen_ai-scoped: no signal, no source.
|
||||
const fieldValuesConfig: FieldValuesConfig = isAIObservability
|
||||
? {
|
||||
name: filter.attributeKey.key,
|
||||
searchText,
|
||||
existingQuery,
|
||||
startUnixMilli,
|
||||
endUnixMilli,
|
||||
}
|
||||
: {
|
||||
signal: filter.dataSource
|
||||
? DATA_SOURCE_TO_SIGNAL[filter.dataSource]
|
||||
: undefined,
|
||||
name: filter.attributeKey.key,
|
||||
searchText,
|
||||
existingQuery,
|
||||
metricNamespace,
|
||||
source: source ? QUICK_FILTERS_SOURCE_TO_SOURCE[source] : undefined,
|
||||
startUnixMilli,
|
||||
// This field does not affect the backend but I wanted to keep it here
|
||||
// in case we add the support in the future
|
||||
endUnixMilli,
|
||||
};
|
||||
|
||||
const {
|
||||
data: values,
|
||||
isLoading,
|
||||
isFetching,
|
||||
} = useFieldValuesSuggestion(fieldValuesConfig, builderQueryType, { enabled });
|
||||
|
||||
const relatedValues: string[] = useMemo(() => {
|
||||
const values = data?.data?.values;
|
||||
if (!values) {
|
||||
return [];
|
||||
}
|
||||
@@ -78,10 +90,9 @@ export function useFieldValues({
|
||||
value !== null && value !== undefined && value !== '',
|
||||
) || []
|
||||
);
|
||||
}, [data]);
|
||||
}, [values]);
|
||||
|
||||
const allValues: string[] = useMemo(() => {
|
||||
const values = data?.data?.values;
|
||||
if (!values) {
|
||||
return [];
|
||||
}
|
||||
@@ -101,7 +112,7 @@ export function useFieldValues({
|
||||
.map((value) => value.toString()) || [];
|
||||
|
||||
return [...stringValues, ...numberValues, ...boolValues];
|
||||
}, [data]);
|
||||
}, [values]);
|
||||
|
||||
return { relatedValues, allValues, isLoading, isFetching };
|
||||
}
|
||||
|
||||
@@ -1,36 +1,32 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Collapse } from 'antd';
|
||||
import { Undo2 } from '@signozhq/icons';
|
||||
import useActiveQueryIndex from 'components/QuickFilters/hooks/useActiveQueryIndex';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||
import { getMs } from 'utils/timeUtils';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
|
||||
import { DurationSection } from 'pages/TracesExplorer/Filter/DurationSection';
|
||||
import {
|
||||
AllTraceFilterKeys,
|
||||
AllTraceFilterKeyValue,
|
||||
HandleRunProps,
|
||||
traceFilterKeys,
|
||||
unionTagFilterItems,
|
||||
} from 'pages/TracesExplorer/Filter/filterUtils';
|
||||
} from 'constants/traceFilterKeys';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { clearFilterFromQuery } from '../shared/filterQuery';
|
||||
import { SectionActionButton } from '../shared/SectionActionButton/SectionActionButton';
|
||||
import { DurationSection } from './DurationSection';
|
||||
import { FilterType, HandleRunProps, unionTagFilterItems } from './utils';
|
||||
|
||||
import './Duration.styles.scss';
|
||||
|
||||
export type FilterType = Record<
|
||||
AllTraceFilterKeys,
|
||||
{ values: string[] | string; keys: BaseAutocompleteData }
|
||||
>;
|
||||
export type { FilterType };
|
||||
|
||||
function Duration({
|
||||
filter,
|
||||
@@ -39,7 +35,7 @@ function Duration({
|
||||
}: {
|
||||
filter: IQuickFiltersConfig;
|
||||
onFilterChange?: (query: Query) => void;
|
||||
source?: QuickFiltersSource;
|
||||
source: QuickFiltersSource;
|
||||
}): JSX.Element {
|
||||
const [selectedFilters, setSelectedFilters] =
|
||||
useState<
|
||||
@@ -52,26 +48,11 @@ function Duration({
|
||||
filter.defaultOpen ? 'durationNano' : '',
|
||||
]);
|
||||
|
||||
const {
|
||||
currentQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
lastUsedQuery,
|
||||
panelType,
|
||||
} = useQueryBuilder();
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
|
||||
const isListView = panelType === PANEL_TYPES.LIST;
|
||||
// In ListView mode, use index 0 for most sources; for TRACES_EXPLORER, use lastUsedQuery
|
||||
// Otherwise use lastUsedQuery for non-ListView modes
|
||||
const activeQueryIndex = useMemo(() => {
|
||||
if (isListView) {
|
||||
return source === QuickFiltersSource.TRACES_EXPLORER
|
||||
? lastUsedQuery || 0
|
||||
: 0;
|
||||
}
|
||||
return lastUsedQuery || 0;
|
||||
}, [isListView, source, lastUsedQuery]);
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const syncSelectedFilters = useMemo((): FilterType => {
|
||||
|
||||
@@ -9,10 +9,12 @@ import {
|
||||
} from 'react';
|
||||
import { Input } from 'antd';
|
||||
import { Slider } from '@signozhq/ui/slider';
|
||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||
import { getMs } from 'utils/timeUtils';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
|
||||
import { addFilter, FilterType, traceFilterKeys } from './filterUtils';
|
||||
import { traceFilterKeys } from 'constants/traceFilterKeys';
|
||||
|
||||
import { addFilter, FilterType } from './utils';
|
||||
|
||||
interface DurationProps {
|
||||
selectedFilters: FilterType | undefined;
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { AllTraceFilterKeys } from 'constants/traceFilterKeys';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export type FilterType = Record<
|
||||
AllTraceFilterKeys,
|
||||
{ values: string[] | string; keys: BaseAutocompleteData }
|
||||
>;
|
||||
|
||||
export interface HandleRunProps {
|
||||
resetAll?: boolean;
|
||||
clearByType?: AllTraceFilterKeys;
|
||||
}
|
||||
|
||||
function convertToStringArr(value: string | string[] | undefined): string[] {
|
||||
if (value) {
|
||||
if (typeof value === 'string') {
|
||||
return [value];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export const addFilter = (
|
||||
filterType: AllTraceFilterKeys,
|
||||
value: string,
|
||||
setSelectedFilters: Dispatch<
|
||||
SetStateAction<
|
||||
| Record<
|
||||
AllTraceFilterKeys,
|
||||
{ values: string[] | string; keys: BaseAutocompleteData }
|
||||
>
|
||||
| undefined
|
||||
>
|
||||
>,
|
||||
keys: BaseAutocompleteData,
|
||||
): void => {
|
||||
setSelectedFilters((prevFilters) => {
|
||||
const isDuration = [
|
||||
'durationNanoMax',
|
||||
'durationNanoMin',
|
||||
'durationNano',
|
||||
].includes(filterType);
|
||||
|
||||
// Convert value to string array
|
||||
const valueArray = convertToStringArr(value);
|
||||
|
||||
// If previous filters are undefined, initialize them
|
||||
if (!prevFilters) {
|
||||
return {
|
||||
[filterType]: { values: isDuration ? value : valueArray, keys },
|
||||
} as unknown as FilterType;
|
||||
}
|
||||
|
||||
// If the filter type doesn't exist, initialize it
|
||||
if (!prevFilters[filterType]?.values.length) {
|
||||
return {
|
||||
...prevFilters,
|
||||
[filterType]: { values: isDuration ? value : valueArray, keys },
|
||||
};
|
||||
}
|
||||
|
||||
// If the value already exists, don't add it again
|
||||
if (convertToStringArr(prevFilters[filterType].values).includes(value)) {
|
||||
return prevFilters;
|
||||
}
|
||||
|
||||
// Otherwise, add the value to the existing array
|
||||
return {
|
||||
...prevFilters,
|
||||
[filterType]: {
|
||||
values: isDuration
|
||||
? value
|
||||
: [...convertToStringArr(prevFilters[filterType].values), value],
|
||||
keys,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** Merges two filter lists; later items win on the same key + operator. */
|
||||
export function unionTagFilterItems(
|
||||
items1: TagFilterItem[],
|
||||
items2: TagFilterItem[],
|
||||
): TagFilterItem[] {
|
||||
const unionMap = new Map<string, TagFilterItem>();
|
||||
|
||||
items1?.forEach((item) => {
|
||||
const keyOp = `${item?.key?.key}_${item?.op}`;
|
||||
unionMap.set(keyOp, item);
|
||||
});
|
||||
|
||||
items2?.forEach((item) => {
|
||||
const keyOp = `${item?.key?.key}_${item?.op}`;
|
||||
unionMap.set(keyOp, item);
|
||||
});
|
||||
|
||||
return Array.from(unionMap?.values());
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import { isFunction } from 'lodash-es';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
|
||||
import useActiveQueryIndex from './hooks/useActiveQueryIndex';
|
||||
import CheckboxV2 from './FilterRenderers/Checkbox/v2/CheckboxFilterV2';
|
||||
import Duration from './FilterRenderers/Duration/Duration';
|
||||
import Slider from './FilterRenderers/Slider/Slider';
|
||||
@@ -113,14 +114,13 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
const shouldShowDropdownInListView =
|
||||
isListView && source === QuickFiltersSource.TRACES_EXPLORER;
|
||||
|
||||
const activeQueryIndex = useMemo(() => {
|
||||
if (isListView) {
|
||||
return source === QuickFiltersSource.TRACES_EXPLORER
|
||||
? lastUsedQuery || 0
|
||||
: 0;
|
||||
}
|
||||
return lastUsedQuery || 0;
|
||||
}, [isListView, source, lastUsedQuery]);
|
||||
// AI observability builds a single query in the row-level views, so there is
|
||||
// no query for the selector to switch between.
|
||||
const isAIObservabilityRowView =
|
||||
source === QuickFiltersSource.AI_OBSERVABILITY &&
|
||||
(isListView || panelType === PANEL_TYPES.TRACE);
|
||||
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
|
||||
// clear all the filters for the query which is in sync with filters
|
||||
const handleReset = (): void => {
|
||||
@@ -167,9 +167,10 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
|
||||
|
||||
// In ListView, always show the 0th query's name; otherwise use the active query's name
|
||||
const displayedQueryName = isListView
|
||||
? showQueryName && currentQuery.builder.queryData?.[0]?.queryName
|
||||
: lastQueryName;
|
||||
const displayedQueryName =
|
||||
isListView || isAIObservabilityRowView
|
||||
? showQueryName && currentQuery.builder.queryData?.[0]?.queryName
|
||||
: lastQueryName;
|
||||
|
||||
const handleQueryChange = (value: number): void => {
|
||||
setLastUsedQuery(value);
|
||||
@@ -182,7 +183,9 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
<Typography.Text className="text">
|
||||
{displayedQueryName ? 'Filters for' : 'Filters'}
|
||||
</Typography.Text>
|
||||
{queryOptions.length > 1 && (!isListView || shouldShowDropdownInListView) ? (
|
||||
{queryOptions.length > 1 &&
|
||||
!isAIObservabilityRowView &&
|
||||
(!isListView || shouldShowDropdownInListView) ? (
|
||||
<Combobox open={open} onOpenChange={setOpen}>
|
||||
<ComboboxTrigger
|
||||
placeholder="Select a query"
|
||||
@@ -318,6 +321,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
return (
|
||||
<Duration
|
||||
key={filter.attributeKey.key}
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import { useGetFieldsKeys } from 'api/generated/services/fields';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { FieldKeysConfig } from 'api/querySuggestions/types';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
|
||||
import {
|
||||
BuilderQueryType,
|
||||
FieldContext,
|
||||
FieldDataType,
|
||||
TelemetryFieldKey,
|
||||
@@ -41,23 +43,31 @@ function OtherFilters({
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
|
||||
}): JSX.Element {
|
||||
const isMeterDataSource = signal === SignalType.METER_EXPLORER;
|
||||
const isAIObservability = signal === SignalType.AI_OBSERVABILITY;
|
||||
|
||||
const { data, isFetching } = useGetFieldsKeys(
|
||||
{
|
||||
searchText: inputValue,
|
||||
signal: signal
|
||||
? DATA_SOURCE_TO_SIGNAL[SIGNAL_DATA_SOURCE_MAP[signal]]
|
||||
: undefined,
|
||||
source: isMeterDataSource ? TelemetrytypesSourceDTO.meter : undefined,
|
||||
},
|
||||
{ query: { enabled: !!signal } },
|
||||
const builderQueryType: BuilderQueryType | undefined = isAIObservability
|
||||
? 'builder_ai_query'
|
||||
: undefined;
|
||||
|
||||
const fieldKeysConfig: FieldKeysConfig = isAIObservability
|
||||
? { searchText: inputValue }
|
||||
: {
|
||||
searchText: inputValue,
|
||||
signal: signal
|
||||
? DATA_SOURCE_TO_SIGNAL[SIGNAL_DATA_SOURCE_MAP[signal]]
|
||||
: undefined,
|
||||
source: isMeterDataSource ? TelemetrytypesSourceDTO.meter : undefined,
|
||||
};
|
||||
|
||||
const { data: fetchedKeys, isFetching } = useFieldKeysSuggestion(
|
||||
fieldKeysConfig,
|
||||
builderQueryType,
|
||||
);
|
||||
|
||||
const otherFilters = useMemo<TelemetryFieldKey[]>(() => {
|
||||
const rawSuggestions = Object.values(data?.data?.keys ?? {}).flat();
|
||||
// Normalize: synthesize the composite `key` once so downstream reads (dedupe,
|
||||
// add, render) can trust it.
|
||||
const suggestions: TelemetryFieldKey[] = rawSuggestions.map((attr) => ({
|
||||
const suggestions: TelemetryFieldKey[] = (fetchedKeys ?? []).map((attr) => ({
|
||||
name: attr.name,
|
||||
signal: attr.signal as TelemetryFieldKey['signal'],
|
||||
fieldContext: attr.fieldContext as FieldContext,
|
||||
@@ -71,7 +81,7 @@ function OtherFilters({
|
||||
),
|
||||
);
|
||||
return suggestions.filter((attr) => !addedKeys.has(attr.key as string));
|
||||
}, [data, addedFilters]);
|
||||
}, [fetchedKeys, addedFilters]);
|
||||
|
||||
const handleAddFilter = (filter: TelemetryFieldKey): void => {
|
||||
setAddedFilters((prev) => [...prev, filter]);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render } from 'tests/test-utils';
|
||||
|
||||
import { SignalType } from '../../types';
|
||||
import OtherFilters from '../OtherFilters';
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
const FIELDS_KEYS_URL = `${BASE_URL}/api/v1/fields/keys`;
|
||||
const AI_KEYS_URL = `${BASE_URL}/api/v1/ai_observability/fields/keys`;
|
||||
|
||||
function keysResponse(name: string): Record<string, unknown> {
|
||||
return {
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: {
|
||||
[name]: [{ name, fieldContext: 'attribute', fieldDataType: 'string' }],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('OtherFilters - AI observability keys', () => {
|
||||
let fieldsKeysCalled: boolean;
|
||||
let aiKeysParams: URLSearchParams | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
fieldsKeysCalled = false;
|
||||
aiKeysParams = undefined;
|
||||
|
||||
server.use(
|
||||
rest.get(FIELDS_KEYS_URL, (_, res, ctx) => {
|
||||
fieldsKeysCalled = true;
|
||||
return res(ctx.status(200), ctx.json(keysResponse('http.route')));
|
||||
}),
|
||||
rest.get(AI_KEYS_URL, (req, res, ctx) => {
|
||||
aiKeysParams = req.url.searchParams;
|
||||
return res(ctx.status(200), ctx.json(keysResponse('gen_ai.request.model')));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
function renderOtherFilters(signal: SignalType): void {
|
||||
render(
|
||||
<OtherFilters
|
||||
signal={signal}
|
||||
inputValue=""
|
||||
addedFilters={[]}
|
||||
setAddedFilters={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
it('reads AI observability keys from their own endpoint', async () => {
|
||||
renderOtherFilters(SignalType.AI_OBSERVABILITY);
|
||||
|
||||
await expect(
|
||||
screen.findByText('gen_ai.request.model'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(fieldsKeysCalled).toBe(false);
|
||||
});
|
||||
|
||||
it('does not narrow the AI keys by fieldContext', async () => {
|
||||
renderOtherFilters(SignalType.AI_OBSERVABILITY);
|
||||
|
||||
// A `trace` context would return only the computed per-trace aggregates,
|
||||
// which cannot be filtered on.
|
||||
await waitFor(() => expect(aiKeysParams).toBeDefined());
|
||||
expect(aiKeysParams?.get('fieldContext')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps other signals on the signal-wide keys endpoint', async () => {
|
||||
renderOtherFilters(SignalType.TRACES);
|
||||
|
||||
await expect(screen.findByText('http.route')).resolves.toBeInTheDocument();
|
||||
await waitFor(() => expect(aiKeysParams).toBeUndefined());
|
||||
});
|
||||
});
|
||||
@@ -7,4 +7,5 @@ export const SIGNAL_DATA_SOURCE_MAP = {
|
||||
[SignalType.EXCEPTIONS]: DataSource.TRACES,
|
||||
[SignalType.API_MONITORING]: DataSource.TRACES,
|
||||
[SignalType.METER_EXPLORER]: DataSource.METRICS,
|
||||
[SignalType.AI_OBSERVABILITY]: DataSource.TRACES,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
import { QuickFiltersSource } from '../../types';
|
||||
import useActiveQueryIndex from '../useActiveQueryIndex';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
}));
|
||||
|
||||
const LAST_USED_QUERY = 2;
|
||||
|
||||
function mockQueryBuilder(panelType: PANEL_TYPES): void {
|
||||
(useQueryBuilder as jest.Mock).mockReturnValue({
|
||||
lastUsedQuery: LAST_USED_QUERY,
|
||||
panelType,
|
||||
});
|
||||
}
|
||||
|
||||
describe('useActiveQueryIndex', () => {
|
||||
describe('AI observability builds a single query in the row-level views', () => {
|
||||
it.each([PANEL_TYPES.LIST, PANEL_TYPES.TRACE])(
|
||||
'drives the first query in %s',
|
||||
(panelType) => {
|
||||
mockQueryBuilder(panelType);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useActiveQueryIndex(QuickFiltersSource.AI_OBSERVABILITY),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([PANEL_TYPES.TIME_SERIES, PANEL_TYPES.TABLE])(
|
||||
'follows the last used query in %s',
|
||||
(panelType) => {
|
||||
mockQueryBuilder(panelType);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useActiveQueryIndex(QuickFiltersSource.AI_OBSERVABILITY),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(LAST_USED_QUERY);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('other sources are unchanged', () => {
|
||||
it('lets the traces explorer track the last used query in list view', () => {
|
||||
mockQueryBuilder(PANEL_TYPES.LIST);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useActiveQueryIndex(QuickFiltersSource.TRACES_EXPLORER),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(LAST_USED_QUERY);
|
||||
});
|
||||
|
||||
it('pins single-query sources to the first query in list view', () => {
|
||||
mockQueryBuilder(PANEL_TYPES.LIST);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useActiveQueryIndex(QuickFiltersSource.INFRA_MONITORING),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(0);
|
||||
});
|
||||
|
||||
it('tracks the last used query outside list view', () => {
|
||||
mockQueryBuilder(PANEL_TYPES.TIME_SERIES);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useActiveQueryIndex(QuickFiltersSource.LOGS_EXPLORER),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(LAST_USED_QUERY);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,13 +15,21 @@ function useActiveQueryIndex(source: QuickFiltersSource): number {
|
||||
const isListView = panelType === PANEL_TYPES.LIST;
|
||||
|
||||
return useMemo(() => {
|
||||
// AI observability builds a single query in the row-level views, so its
|
||||
// filters always drive the first one there.
|
||||
if (source === QuickFiltersSource.AI_OBSERVABILITY) {
|
||||
return isListView || panelType === PANEL_TYPES.TRACE
|
||||
? 0
|
||||
: lastUsedQuery || 0;
|
||||
}
|
||||
|
||||
if (isListView) {
|
||||
return source === QuickFiltersSource.TRACES_EXPLORER
|
||||
? lastUsedQuery || 0
|
||||
: 0;
|
||||
}
|
||||
return lastUsedQuery || 0;
|
||||
}, [isListView, source, lastUsedQuery]);
|
||||
}, [isListView, panelType, source, lastUsedQuery]);
|
||||
}
|
||||
|
||||
export default useActiveQueryIndex;
|
||||
@@ -24,6 +24,7 @@ export enum SignalType {
|
||||
API_MONITORING = 'api_monitoring',
|
||||
EXCEPTIONS = 'exceptions',
|
||||
METER_EXPLORER = 'meter',
|
||||
AI_OBSERVABILITY = 'ai_observability',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,6 +70,7 @@ export enum QuickFiltersSource {
|
||||
API_MONITORING = 'api-monitoring',
|
||||
EXCEPTIONS = 'exceptions',
|
||||
METER_EXPLORER = 'meter',
|
||||
AI_OBSERVABILITY = 'ai-observability',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,7 +41,6 @@ export enum LOCALSTORAGE {
|
||||
DISMISSED_API_KEYS_DEPRECATION_BANNER = 'DISMISSED_API_KEYS_DEPRECATION_BANNER',
|
||||
TRACE_DETAILS_SPAN_DETAILS_POSITION = 'TRACE_DETAILS_SPAN_DETAILS_POSITION',
|
||||
LICENSE_KEY_CALLOUT_DISMISSED = 'LICENSE_KEY_CALLOUT_DISMISSED',
|
||||
TRACE_DETAILS_PREFER_OLD_VIEW = 'TRACE_DETAILS_PREFER_OLD_VIEW',
|
||||
DASHBOARD_PREFERENCES = 'DASHBOARD_PREFERENCES',
|
||||
ACTIVE_SIGNOZ_INSTANCE_URL = 'ACTIVE_SIGNOZ_INSTANCE_URL',
|
||||
DASHBOARDS_LIST_VISIBLE_COLUMNS = 'DASHBOARDS_LIST_VISIBLE_COLUMNS',
|
||||
|
||||
@@ -109,6 +109,9 @@ export const REACT_QUERY_KEY = {
|
||||
// Field Keys Suggestion Query Keys
|
||||
FIELD_KEYS_SUGGESTION: 'FIELD_KEYS_SUGGESTION',
|
||||
|
||||
// Field Values Suggestion Query Keys
|
||||
FIELD_VALUES_SUGGESTION: 'FIELD_VALUES_SUGGESTION',
|
||||
|
||||
// AI Assistant Query Keys
|
||||
AI_ASSISTANT_EMPTY_STATE_CHIPS: 'AI_ASSISTANT_EMPTY_STATE_CHIPS',
|
||||
} as const;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OperatorValues } from 'types/reducer/trace';
|
||||
import { OperatorValues } from 'hooks/useResourceAttribute/types';
|
||||
|
||||
export const OperatorConversions: Array<{
|
||||
label: string;
|
||||
|
||||
@@ -6,9 +6,7 @@ const ROUTES = {
|
||||
SERVICE_METRICS: '/services/:servicename',
|
||||
SERVICE_TOP_LEVEL_OPERATIONS: '/services/:servicename/top-level-operations',
|
||||
SERVICE_MAP: '/service-map',
|
||||
TRACE: '/trace',
|
||||
TRACE_DETAIL: '/trace/:id',
|
||||
TRACE_DETAIL_OLD: '/trace-old/:id',
|
||||
TRACES_EXPLORER: '/traces-explorer',
|
||||
ONBOARDING: '/onboarding',
|
||||
GET_STARTED_WITH_CLOUD: '/get-started-with-signoz-cloud',
|
||||
|
||||
124
frontend/src/constants/traceFilterKeys.ts
Normal file
124
frontend/src/constants/traceFilterKeys.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { SPAN_ATTRIBUTES } from 'container/ApiMonitoring/Explorer/Domains/DomainDetails/constants';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
/** Trace attribute key -> label shown in quick filters, the options menu and APM "view traces" links. */
|
||||
export const AllTraceFilterKeyValue: Record<string, string> = {
|
||||
durationNanoMin: 'Duration',
|
||||
durationNano: 'Duration',
|
||||
duration_nano: 'Duration',
|
||||
durationNanoMax: 'Duration',
|
||||
'deployment.environment': 'Environment',
|
||||
hasError: 'Status',
|
||||
has_error: 'Status',
|
||||
serviceName: 'Service Name',
|
||||
'service.name': 'service.name',
|
||||
name: 'Operation / Name',
|
||||
rpcMethod: 'RPC Method',
|
||||
'rpc.method': 'RPC Method',
|
||||
responseStatusCode: 'Status Code',
|
||||
response_status_code: 'Status Code',
|
||||
httpHost: 'HTTP Host',
|
||||
http_host: 'HTTP Host',
|
||||
httpMethod: 'HTTP Method',
|
||||
http_method: 'HTTP Method',
|
||||
httpRoute: 'HTTP Route',
|
||||
'http.route': 'HTTP Route',
|
||||
httpUrl: 'HTTP URL',
|
||||
[SPAN_ATTRIBUTES.HTTP_URL]: 'HTTP URL',
|
||||
traceID: 'Trace ID',
|
||||
trace_id: 'Trace ID',
|
||||
} as const;
|
||||
|
||||
export type AllTraceFilterKeys = keyof typeof AllTraceFilterKeyValue;
|
||||
|
||||
export const traceFilterKeys: Record<AllTraceFilterKeys, BaseAutocompleteData> =
|
||||
{
|
||||
durationNano: {
|
||||
key: 'durationNano',
|
||||
dataType: DataTypes.Float64,
|
||||
type: 'tag',
|
||||
id: 'durationNano--float64--tag--true',
|
||||
},
|
||||
hasError: {
|
||||
key: 'hasError',
|
||||
dataType: DataTypes.bool,
|
||||
type: 'tag',
|
||||
id: 'hasError--bool--tag--true',
|
||||
},
|
||||
serviceName: {
|
||||
key: 'serviceName',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
id: 'serviceName--string--tag--true',
|
||||
},
|
||||
|
||||
'deployment.environment': {
|
||||
key: 'deployment.environment',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'deployment.environment--string--resource--false',
|
||||
},
|
||||
name: {
|
||||
key: 'name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
id: 'name--string--tag--true',
|
||||
},
|
||||
rpcMethod: {
|
||||
key: 'rpcMethod',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
id: 'rpcMethod--string--tag--true',
|
||||
},
|
||||
responseStatusCode: {
|
||||
key: 'responseStatusCode',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
id: 'responseStatusCode--string--tag--true',
|
||||
},
|
||||
httpHost: {
|
||||
key: 'httpHost',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
id: 'httpHost--string--tag--true',
|
||||
},
|
||||
httpMethod: {
|
||||
key: 'httpMethod',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
id: 'httpMethod--string--tag--true',
|
||||
},
|
||||
httpRoute: {
|
||||
key: 'httpRoute',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
id: 'httpRoute--string--tag--true',
|
||||
},
|
||||
httpUrl: {
|
||||
key: 'httpUrl',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
id: 'httpUrl--string--tag--true',
|
||||
},
|
||||
traceID: {
|
||||
key: 'traceID',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
id: 'traceID--string--tag--true',
|
||||
},
|
||||
durationNanoMin: {
|
||||
key: 'durationNanoMin',
|
||||
dataType: DataTypes.Float64,
|
||||
type: 'tag',
|
||||
id: 'durationNanoMin--float64--tag--true',
|
||||
},
|
||||
durationNanoMax: {
|
||||
key: 'durationNanoMax',
|
||||
dataType: DataTypes.Float64,
|
||||
type: 'tag',
|
||||
id: 'durationNanoMax--float64--tag--true',
|
||||
},
|
||||
} as const;
|
||||
@@ -1,16 +0,0 @@
|
||||
.span-container {
|
||||
.spanDetails {
|
||||
position: absolute;
|
||||
height: 50px;
|
||||
padding: 8px;
|
||||
min-width: 150px;
|
||||
background: lightcyan;
|
||||
color: black;
|
||||
bottom: 24px;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Popover } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { toFixed } from 'utils/toFixed';
|
||||
|
||||
import { SpanBorder, SpanLine, SpanText, SpanWrapper } from './styles';
|
||||
|
||||
import '../GantChart.styles.scss';
|
||||
|
||||
interface SpanLengthProps {
|
||||
globalStart: number;
|
||||
startTime: number;
|
||||
name: string;
|
||||
width: string;
|
||||
leftOffset: string;
|
||||
bgColor: string;
|
||||
inMsCount: number;
|
||||
}
|
||||
|
||||
function Span(props: SpanLengthProps): JSX.Element {
|
||||
const { width, leftOffset, bgColor, inMsCount, startTime, name, globalStart } =
|
||||
props;
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.scrollTop = document.documentElement.clientHeight;
|
||||
document.documentElement.scrollLeft = document.documentElement.clientWidth;
|
||||
}, []);
|
||||
|
||||
const getContent = (): JSX.Element => {
|
||||
const timeStamp = dayjs(startTime)
|
||||
.tz(timezone.value)
|
||||
.format(DATE_TIME_FORMATS.TIME_UTC_MS);
|
||||
const startTimeInMs = startTime - globalStart;
|
||||
return (
|
||||
<div>
|
||||
<Typography.Text style={{ marginBottom: '8px' }}>
|
||||
{' '}
|
||||
Duration : {inMsCount}
|
||||
</Typography.Text>
|
||||
<br />
|
||||
<Typography.Text style={{ marginBottom: '8px' }}>
|
||||
Start Time: {startTimeInMs}ms [{timeStamp}]{' '}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SpanWrapper className="span-container">
|
||||
<SpanLine
|
||||
className="spanLine"
|
||||
isDarkMode={isDarkMode}
|
||||
bgColor={bgColor}
|
||||
leftOffset={leftOffset}
|
||||
width={width}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Popover
|
||||
style={{
|
||||
left: `${leftOffset}%`,
|
||||
}}
|
||||
title={name}
|
||||
content={getContent()}
|
||||
trigger="hover"
|
||||
placement="left"
|
||||
autoAdjustOverflow
|
||||
>
|
||||
<SpanBorder
|
||||
className="spanTrack"
|
||||
isDarkMode={isDarkMode}
|
||||
bgColor={bgColor}
|
||||
leftOffset={leftOffset}
|
||||
width={width}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<SpanText isDarkMode={isDarkMode} leftOffset={leftOffset}>{`${toFixed(
|
||||
time,
|
||||
2,
|
||||
)} ${timeUnitName}`}</SpanText>
|
||||
</SpanWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default Span;
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface Props {
|
||||
width: string;
|
||||
leftOffset: string;
|
||||
bgColor: string;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const SpanLine = styled.div<Props>`
|
||||
width: ${({ leftOffset }): string => `${leftOffset}%`};
|
||||
height: 0px;
|
||||
border-bottom: 0.1px solid
|
||||
${({ isDarkMode }): string => (isDarkMode ? '#303030' : '#c0c0c0')};
|
||||
top: 50%;
|
||||
position: absolute;
|
||||
`;
|
||||
|
||||
export const SpanBorder = styled.div<Props>`
|
||||
background: ${({ bgColor }): string => bgColor};
|
||||
border-radius: 5px;
|
||||
height: 0.625rem;
|
||||
width: ${({ width }): string => `${width}%`};
|
||||
left: ${({ leftOffset }): string => `${leftOffset}%`};
|
||||
top: 35%;
|
||||
position: absolute;
|
||||
`;
|
||||
|
||||
export const SpanWrapper = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
min-height: 2rem;
|
||||
`;
|
||||
interface SpanTextProps extends Pick<Props, 'leftOffset'> {
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const SpanText = styled(Typography.Text)<SpanTextProps>`
|
||||
&&& {
|
||||
left: ${({ leftOffset }): string => `${leftOffset}%`};
|
||||
top: 65%;
|
||||
position: absolute;
|
||||
width: max-content;
|
||||
color: ${({ isDarkMode }): string => (isDarkMode ? '#ACACAC' : '#666')};
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
`;
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Container, Service, Span, SpanWrapper } from './styles';
|
||||
|
||||
function SpanNameComponent({
|
||||
name,
|
||||
serviceName,
|
||||
}: SpanNameComponentProps): JSX.Element {
|
||||
return (
|
||||
<Container title={`${name} ${serviceName}`}>
|
||||
<SpanWrapper>
|
||||
<Span truncate={1}>{name}</Span>
|
||||
<Service truncate={1}>{serviceName}</Service>
|
||||
</SpanWrapper>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
interface SpanNameComponentProps {
|
||||
name: string;
|
||||
serviceName: string;
|
||||
}
|
||||
|
||||
export default SpanNameComponent;
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Span = styled(Typography.Text)`
|
||||
&&& {
|
||||
font-size: 0.75rem;
|
||||
margin: 0;
|
||||
/* border-bottom: 1px solid grey; */
|
||||
}
|
||||
`;
|
||||
|
||||
export const Service = styled(Typography.Text)`
|
||||
&&& {
|
||||
color: #acacac;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const SpanWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 0.625rem;
|
||||
width: 10rem;
|
||||
`;
|
||||
|
||||
export const SpanConnector = styled.div`
|
||||
width: 37px;
|
||||
border: 1px solid #303030;
|
||||
height: 0;
|
||||
`;
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
`;
|
||||
|
||||
export const SpanName = styled.div`
|
||||
width: fit-content;
|
||||
border-bottom: 1px solid black;
|
||||
`;
|
||||
@@ -1,233 +0,0 @@
|
||||
import {
|
||||
Dispatch,
|
||||
MouseEventHandler,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
import { Col } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { StyledCol, StyledRow } from 'components/Styled';
|
||||
import {
|
||||
IIntervalUnit,
|
||||
SPAN_DETAILS_LEFT_COL_WIDTH,
|
||||
} from 'container/TraceDetail/utils';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { ITraceTree } from 'types/api/trace/getTraceItem';
|
||||
|
||||
import { ITraceMetaData } from '..';
|
||||
import Span from '../Span';
|
||||
import SpanName from '../SpanName';
|
||||
import { getMetaDataFromSpanTree, getTopLeftFromBody } from '../utils';
|
||||
import {
|
||||
CardComponent,
|
||||
CardContainer,
|
||||
CaretContainer,
|
||||
HoverCard,
|
||||
styles,
|
||||
Wrapper,
|
||||
} from './styles';
|
||||
import { getIconStyles } from './utils';
|
||||
|
||||
function Trace(props: TraceProps): JSX.Element {
|
||||
const {
|
||||
name,
|
||||
activeHoverId,
|
||||
setActiveHoverId,
|
||||
globalSpread,
|
||||
globalStart,
|
||||
serviceName,
|
||||
startTime,
|
||||
value,
|
||||
serviceColour,
|
||||
id,
|
||||
setActiveSelectedId,
|
||||
activeSelectedId,
|
||||
level,
|
||||
activeSpanPath,
|
||||
isExpandAll,
|
||||
intervalUnit,
|
||||
children,
|
||||
isMissing,
|
||||
} = props;
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const [isOpen, setOpen] = useState<boolean>(activeSpanPath[level] === id);
|
||||
|
||||
const localTreeExpandInteraction = useRef<boolean | 0>(0); // Boolean is for the state of the expansion whereas the number i.e. 0 is for skipping the user interaction.
|
||||
|
||||
useEffect(() => {
|
||||
if (localTreeExpandInteraction.current !== 0) {
|
||||
setOpen(localTreeExpandInteraction.current);
|
||||
localTreeExpandInteraction.current = 0;
|
||||
} else if (!isOpen) {
|
||||
setOpen(activeSpanPath[level] === id);
|
||||
}
|
||||
}, [activeSpanPath, isOpen, id, level]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpandAll) {
|
||||
setOpen(isExpandAll);
|
||||
} else {
|
||||
setOpen(activeSpanPath[level] === id);
|
||||
}
|
||||
}, [isExpandAll, activeSpanPath, id, level]);
|
||||
|
||||
const isOnlyChild = children.length === 1;
|
||||
const [top, setTop] = useState<number>(0);
|
||||
|
||||
const ref = useRef<HTMLUListElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeSelectedId === id) {
|
||||
ref.current?.scrollIntoView({
|
||||
block: 'nearest',
|
||||
behavior: 'auto',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
}, [activeSelectedId, id]);
|
||||
|
||||
const onMouseEnterHandler = (): void => {
|
||||
setActiveHoverId(id);
|
||||
if (ref.current) {
|
||||
const { top } = getTopLeftFromBody(ref.current);
|
||||
setTop(top);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseLeaveHandler = (): void => {
|
||||
setActiveHoverId('');
|
||||
};
|
||||
|
||||
const onClick = (): void => {
|
||||
setActiveSelectedId(id);
|
||||
};
|
||||
|
||||
const onClickTreeExpansion: MouseEventHandler<HTMLDivElement> = (
|
||||
event,
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
setOpen((state) => {
|
||||
localTreeExpandInteraction.current = !isOpen;
|
||||
return !state;
|
||||
});
|
||||
};
|
||||
const { totalSpans } = getMetaDataFromSpanTree(props);
|
||||
|
||||
const inMsCount = value;
|
||||
const nodeLeftOffset = ((startTime - globalStart) * 1e2) / globalSpread;
|
||||
const width = (value * 1e2) / (globalSpread * 1e6);
|
||||
const panelWidth = SPAN_DETAILS_LEFT_COL_WIDTH - level * (16 + 1) - 48;
|
||||
|
||||
const iconStyles = useMemo(() => getIconStyles(), []);
|
||||
|
||||
const icon = useMemo(
|
||||
() =>
|
||||
isOpen ? (
|
||||
<ChevronDown size="md" style={iconStyles} />
|
||||
) : (
|
||||
<ChevronRight size="md" style={iconStyles} />
|
||||
),
|
||||
[isOpen, iconStyles],
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
onMouseEnter={onMouseEnterHandler}
|
||||
onMouseLeave={onMouseLeaveHandler}
|
||||
isOnlyChild={isOnlyChild}
|
||||
ref={ref}
|
||||
isDarkMode={isDarkMode}
|
||||
>
|
||||
<HoverCard
|
||||
top={top}
|
||||
isHovered={activeHoverId === id}
|
||||
isSelected={activeSelectedId === id}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
|
||||
<CardContainer isMissing={isMissing} onClick={onClick}>
|
||||
<StyledCol flex={`${panelWidth}px`} styledclass={[styles.overFlowHidden]}>
|
||||
<StyledRow styledclass={[styles.flexNoWrap]}>
|
||||
<Col>
|
||||
{totalSpans !== 1 && (
|
||||
<CardComponent
|
||||
isOnlyChild={isOnlyChild}
|
||||
isDarkMode={isDarkMode}
|
||||
onClick={onClickTreeExpansion}
|
||||
>
|
||||
<Typography style={{ wordBreak: 'normal' }}>{totalSpans}</Typography>
|
||||
<CaretContainer>{icon}</CaretContainer>
|
||||
</CardComponent>
|
||||
)}
|
||||
</Col>
|
||||
<Col>
|
||||
<SpanName name={name} serviceName={serviceName} />
|
||||
</Col>
|
||||
</StyledRow>
|
||||
</StyledCol>
|
||||
<Col flex="1">
|
||||
<Span
|
||||
globalStart={globalStart}
|
||||
startTime={startTime}
|
||||
name={name}
|
||||
leftOffset={nodeLeftOffset.toString()}
|
||||
width={width.toString()}
|
||||
bgColor={serviceColour}
|
||||
inMsCount={inMsCount / 1e6}
|
||||
/>
|
||||
</Col>
|
||||
</CardContainer>
|
||||
|
||||
{isOpen && (
|
||||
<>
|
||||
{children.map((child) => (
|
||||
<Trace
|
||||
key={child.id}
|
||||
activeHoverId={activeHoverId}
|
||||
setActiveHoverId={setActiveHoverId}
|
||||
{...child}
|
||||
globalSpread={globalSpread}
|
||||
globalStart={globalStart}
|
||||
setActiveSelectedId={setActiveSelectedId}
|
||||
activeSelectedId={activeSelectedId}
|
||||
level={level + 1}
|
||||
activeSpanPath={activeSpanPath}
|
||||
isExpandAll={isExpandAll}
|
||||
intervalUnit={intervalUnit}
|
||||
isMissing={child.isMissing}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
Trace.defaultProps = {
|
||||
isMissing: false,
|
||||
};
|
||||
|
||||
interface ITraceGlobal {
|
||||
globalSpread: ITraceMetaData['spread'];
|
||||
globalStart: ITraceMetaData['globalStart'];
|
||||
}
|
||||
|
||||
interface TraceProps extends ITraceTree, ITraceGlobal {
|
||||
activeHoverId: string;
|
||||
setActiveHoverId: Dispatch<SetStateAction<string>>;
|
||||
setActiveSelectedId: Dispatch<SetStateAction<string>>;
|
||||
activeSelectedId: string;
|
||||
level: number;
|
||||
activeSpanPath: string[];
|
||||
isExpandAll: boolean;
|
||||
intervalUnit: IIntervalUnit;
|
||||
isMissing?: boolean;
|
||||
}
|
||||
|
||||
export default Trace;
|
||||
@@ -1,113 +0,0 @@
|
||||
import { volcano } from '@ant-design/colors';
|
||||
import styled, {
|
||||
css,
|
||||
DefaultTheme,
|
||||
ThemedCssFunction,
|
||||
} from 'styled-components';
|
||||
|
||||
interface Props {
|
||||
isOnlyChild: boolean;
|
||||
}
|
||||
|
||||
export const Wrapper = styled.ul<Props>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
ul {
|
||||
border-left: ${({ isOnlyChild }): StyledCSS =>
|
||||
isOnlyChild && 'none'} !important;
|
||||
|
||||
${({ isOnlyChild }): StyledCSS =>
|
||||
isOnlyChild &&
|
||||
css`
|
||||
&:before {
|
||||
border-left: 1px solid #434343;
|
||||
display: inline-block;
|
||||
content: '';
|
||||
height: 54px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -35px;
|
||||
}
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CardContainer = styled.li<{ isMissing?: boolean }>`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem;
|
||||
z-index: 2;
|
||||
${({ isMissing }): string =>
|
||||
isMissing ? `border: 1px dashed ${volcano[6]} !important;` : ''}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export type StyledCSS =
|
||||
| ReturnType<ThemedCssFunction<DefaultTheme>>
|
||||
| string
|
||||
| false
|
||||
| undefined;
|
||||
|
||||
export const CardComponent = styled.div<Props>`
|
||||
border: 1px solid
|
||||
${({ isDarkMode }): StyledCSS => (isDarkMode ? '#434343' : '#333')};
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1px 8px;
|
||||
background: ${({ isDarkMode }): StyledCSS =>
|
||||
isDarkMode ? '#1d1d1d' : '#ddd'};
|
||||
height: 22px;
|
||||
`;
|
||||
|
||||
export const CaretContainer = styled.span`
|
||||
margin-left: 0.304rem;
|
||||
`;
|
||||
|
||||
interface HoverCardProps {
|
||||
isHovered: boolean;
|
||||
isSelected: boolean;
|
||||
top: number;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const HoverCard = styled.div<HoverCardProps>`
|
||||
display: ${({ isSelected, isHovered }): string =>
|
||||
isSelected || isHovered ? 'block' : 'none'};
|
||||
width: 200%;
|
||||
background-color: ${({ isHovered, isDarkMode }): string => {
|
||||
if (isHovered) {
|
||||
return isDarkMode ? '#262626' : '#ddd';
|
||||
}
|
||||
return isDarkMode ? '#4f4f4f' : '#bbb';
|
||||
}};
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
right: 0;
|
||||
height: 3rem;
|
||||
opacity: 0.5;
|
||||
`;
|
||||
|
||||
const flexNoWrap = css`
|
||||
flex-wrap: nowrap;
|
||||
`;
|
||||
|
||||
const overFlowHidden = css`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const styles = {
|
||||
flexNoWrap,
|
||||
overFlowHidden,
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export const getIconStyles = (): Record<string, string> => ({
|
||||
color: 'var(--l1-foreground)',
|
||||
});
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { SquareMinus, SquarePlus } from '@signozhq/icons';
|
||||
import { IIntervalUnit } from 'container/TraceDetail/utils';
|
||||
import { ITraceTree } from 'types/api/trace/getTraceItem';
|
||||
|
||||
import { CardContainer, CardWrapper, CollapseButton } from './styles';
|
||||
import Trace from './Trace';
|
||||
import { getSpanPath } from './utils';
|
||||
|
||||
function GanttChart(props: GanttChartProps): JSX.Element {
|
||||
const {
|
||||
data,
|
||||
traceMetaData,
|
||||
activeHoverId,
|
||||
setActiveHoverId,
|
||||
activeSelectedId,
|
||||
setActiveSelectedId,
|
||||
spanId,
|
||||
intervalUnit,
|
||||
} = props;
|
||||
|
||||
const { globalStart, spread: globalSpread } = traceMetaData;
|
||||
|
||||
const [isExpandAll, setIsExpandAll] = useState<boolean>(false);
|
||||
const [activeSpanPath, setActiveSpanPath] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveSpanPath(getSpanPath(data, spanId));
|
||||
}, [spanId, data]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveSpanPath(getSpanPath(data, activeSelectedId));
|
||||
}, [activeSelectedId, data]);
|
||||
|
||||
const handleCollapse = (): void => {
|
||||
setIsExpandAll((prev) => !prev);
|
||||
};
|
||||
return (
|
||||
<CardContainer>
|
||||
<CollapseButton
|
||||
onClick={handleCollapse}
|
||||
title={isExpandAll ? 'Collapse All' : 'Expand All'}
|
||||
>
|
||||
{isExpandAll ? (
|
||||
<SquareMinus size={16} style={{ color: 'var(--accent-primary)' }} />
|
||||
) : (
|
||||
<SquarePlus size={16} style={{ color: 'var(--accent-primary)' }} />
|
||||
)}
|
||||
</CollapseButton>
|
||||
<CardWrapper>
|
||||
<Trace
|
||||
activeHoverId={activeHoverId}
|
||||
activeSpanPath={activeSpanPath}
|
||||
setActiveHoverId={setActiveHoverId}
|
||||
key={data.id}
|
||||
{...{
|
||||
...data,
|
||||
globalSpread,
|
||||
globalStart,
|
||||
setActiveSelectedId,
|
||||
activeSelectedId,
|
||||
}}
|
||||
level={0}
|
||||
isExpandAll={isExpandAll}
|
||||
intervalUnit={intervalUnit}
|
||||
/>
|
||||
</CardWrapper>
|
||||
</CardContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ITraceMetaData {
|
||||
globalEnd: number;
|
||||
globalStart: number;
|
||||
levels: number;
|
||||
spread: number;
|
||||
totalSpans: number;
|
||||
}
|
||||
|
||||
export interface GanttChartProps {
|
||||
data: ITraceTree;
|
||||
traceMetaData: ITraceMetaData;
|
||||
activeSelectedId: string;
|
||||
activeHoverId: string;
|
||||
setActiveHoverId: Dispatch<SetStateAction<string>>;
|
||||
setActiveSelectedId: Dispatch<SetStateAction<string>>;
|
||||
spanId: string;
|
||||
intervalUnit: IIntervalUnit;
|
||||
}
|
||||
|
||||
export default GanttChart;
|
||||
@@ -1,48 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.ul`
|
||||
padding-left: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
border-left: 1px solid #434343;
|
||||
padding-left: 1rem;
|
||||
width: 100%;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
ul li {
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
left: -1rem;
|
||||
top: 10px;
|
||||
content: '';
|
||||
height: 1px;
|
||||
width: 1rem;
|
||||
background-color: #434343;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CardWrapper = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-left: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
`;
|
||||
|
||||
export const CardContainer = styled.li`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const CollapseButton = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
`;
|
||||
@@ -1,203 +0,0 @@
|
||||
import { set } from 'lodash-es';
|
||||
import { ITraceForest, ITraceTree } from 'types/api/trace/getTraceItem';
|
||||
|
||||
interface GetTraceMetaData {
|
||||
globalStart: number;
|
||||
globalEnd: number;
|
||||
spread: number;
|
||||
totalSpans: number;
|
||||
levels: number;
|
||||
}
|
||||
export const getMetaDataFromSpanTree = (
|
||||
treeData: ITraceTree,
|
||||
): GetTraceMetaData => {
|
||||
let globalStart = Number.POSITIVE_INFINITY;
|
||||
let globalEnd = Number.NEGATIVE_INFINITY;
|
||||
let totalSpans = 0;
|
||||
let levels = 1;
|
||||
const traverse = (treeNode: ITraceTree, level = 0): void => {
|
||||
if (!treeNode) {
|
||||
return;
|
||||
}
|
||||
totalSpans += 1;
|
||||
levels = Math.max(levels, level);
|
||||
const { startTime } = treeNode;
|
||||
const endTime = startTime + treeNode.value;
|
||||
globalStart = Math.min(globalStart, startTime);
|
||||
globalEnd = Math.max(globalEnd, endTime);
|
||||
|
||||
treeNode.children.forEach((childNode) => {
|
||||
traverse(childNode, level + 1);
|
||||
});
|
||||
};
|
||||
traverse(treeData, 1);
|
||||
|
||||
globalStart *= 1e6;
|
||||
globalEnd *= 1e6;
|
||||
|
||||
return {
|
||||
globalStart,
|
||||
globalEnd,
|
||||
spread: globalEnd - globalStart,
|
||||
totalSpans,
|
||||
levels,
|
||||
};
|
||||
};
|
||||
|
||||
export function getTopLeftFromBody(elem: HTMLElement): {
|
||||
top: number;
|
||||
left: number;
|
||||
} {
|
||||
const box = elem.getBoundingClientRect();
|
||||
|
||||
const { body } = document;
|
||||
const docEl = document.documentElement;
|
||||
|
||||
const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
|
||||
const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
|
||||
|
||||
const clientTop = docEl.clientTop || body.clientTop || 0;
|
||||
const clientLeft = docEl.clientLeft || body.clientLeft || 0;
|
||||
|
||||
const top = box.top + scrollTop - clientTop;
|
||||
const left = box.left + scrollLeft - clientLeft;
|
||||
|
||||
return { top: Math.round(top), left: Math.round(left) };
|
||||
}
|
||||
|
||||
export const getNodeById = (
|
||||
searchingId: string,
|
||||
treesData: ITraceForest | undefined,
|
||||
): ITraceForest => {
|
||||
const newtreeData: ITraceForest = {} as ITraceForest;
|
||||
|
||||
const traverse = (
|
||||
treeNode: ITraceTree,
|
||||
setCallBack: (arg0: ITraceTree) => void,
|
||||
level = 0,
|
||||
): void => {
|
||||
if (!treeNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (searchingId === treeNode.id) {
|
||||
setCallBack(treeNode);
|
||||
}
|
||||
|
||||
treeNode.children.forEach((childNode) => {
|
||||
traverse(childNode, setCallBack, level + 1);
|
||||
});
|
||||
};
|
||||
|
||||
const spanTreeSetCallback = (
|
||||
path: (keyof ITraceForest)[],
|
||||
value: ITraceTree,
|
||||
): ITraceForest => set(newtreeData, path, [value]);
|
||||
|
||||
if (treesData?.spanTree) {
|
||||
treesData.spanTree.forEach((tree) => {
|
||||
traverse(tree, (value) => spanTreeSetCallback(['spanTree'], value), 1);
|
||||
});
|
||||
}
|
||||
|
||||
if (treesData?.missingSpanTree) {
|
||||
treesData.missingSpanTree.forEach((tree) => {
|
||||
traverse(
|
||||
tree,
|
||||
(value) => spanTreeSetCallback(['missingSpanTree'], value),
|
||||
1,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return newtreeData;
|
||||
};
|
||||
|
||||
const getSpanWithoutChildren = (
|
||||
span: ITraceTree,
|
||||
): Omit<ITraceTree, 'children'> => ({
|
||||
id: span.id,
|
||||
name: span.name,
|
||||
parent: span.parent,
|
||||
serviceColour: span.serviceColour,
|
||||
serviceName: span.serviceName,
|
||||
startTime: span.startTime,
|
||||
tags: span.tags,
|
||||
time: span.time,
|
||||
value: span.value,
|
||||
event: span.event,
|
||||
hasError: span.hasError,
|
||||
spanKind: span.spanKind,
|
||||
statusCodeString: span.statusCodeString,
|
||||
statusMessage: span.statusMessage,
|
||||
});
|
||||
|
||||
export const isSpanPresentInSearchString = (
|
||||
searchedString: string,
|
||||
tree: ITraceTree,
|
||||
): boolean => {
|
||||
const parsedTree = getSpanWithoutChildren(tree);
|
||||
|
||||
const stringifyTree = JSON.stringify(parsedTree);
|
||||
|
||||
return stringifyTree.includes(searchedString);
|
||||
};
|
||||
|
||||
export const isSpanPresent = (
|
||||
tree: ITraceTree,
|
||||
searchedKey: string,
|
||||
): ITraceTree[] => {
|
||||
const foundNode: ITraceTree[] = [];
|
||||
|
||||
const traverse = (
|
||||
treeNode: ITraceTree,
|
||||
level = 0,
|
||||
foundNode: ITraceTree[],
|
||||
): void => {
|
||||
if (!treeNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isPresent = isSpanPresentInSearchString(searchedKey, treeNode);
|
||||
|
||||
if (isPresent) {
|
||||
foundNode.push(treeNode);
|
||||
}
|
||||
|
||||
treeNode.children.forEach((childNode) => {
|
||||
traverse(childNode, level + 1, foundNode);
|
||||
});
|
||||
};
|
||||
traverse(tree, 1, foundNode);
|
||||
|
||||
return foundNode;
|
||||
};
|
||||
|
||||
export const getSpanPath = (tree: ITraceTree, spanId: string): string[] => {
|
||||
const spanPath: string[] = [];
|
||||
|
||||
const traverse = (treeNode: ITraceTree): boolean => {
|
||||
if (!treeNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
spanPath.push(treeNode.id);
|
||||
|
||||
if (spanId === treeNode.id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let foundInChild = false;
|
||||
treeNode.children.forEach((childNode) => {
|
||||
if (traverse(childNode)) {
|
||||
foundInChild = true;
|
||||
}
|
||||
});
|
||||
if (!foundInChild) {
|
||||
spanPath.pop();
|
||||
}
|
||||
return foundInChild;
|
||||
};
|
||||
traverse(tree);
|
||||
return spanPath;
|
||||
};
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from 'types/api/licensesV3/getActive';
|
||||
import { ServicesList } from 'types/api/metrics/getService';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { Tags } from 'types/reducer/trace';
|
||||
import { Tags } from 'hooks/useResourceAttribute/types';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Badge } from '@signozhq/ui/badge';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge';
|
||||
import { TextNoData } from '../../components/TextNoData';
|
||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||
import { getMs } from 'utils/timeUtils';
|
||||
import {
|
||||
BlockLink,
|
||||
getTraceLink,
|
||||
|
||||
@@ -333,6 +333,7 @@ describe('AttributeMappingsTab (integration)', () => {
|
||||
context: FieldContext.attribute,
|
||||
operation: MapperOperation.copy,
|
||||
priority,
|
||||
enabled: true,
|
||||
})),
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import { ConditionKey } from 'container/LLMObservability/AttributeMapping/types';
|
||||
|
||||
import styles from './ConditionsTooltip.module.scss';
|
||||
|
||||
interface ConditionsTooltipProps {
|
||||
attributes: string[];
|
||||
resource: string[];
|
||||
attributes: ConditionKey[];
|
||||
resource: ConditionKey[];
|
||||
}
|
||||
|
||||
function ConditionsTooltip({
|
||||
@@ -33,8 +35,8 @@ function ConditionsTooltip({
|
||||
</Typography.Text>
|
||||
<div className={styles.keyList}>
|
||||
{attributes.map((key) => (
|
||||
<code key={key} className={styles.key}>
|
||||
{key}
|
||||
<code key={`${key.origin}-${key.value}`} className={styles.key}>
|
||||
{key.value}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
@@ -47,8 +49,8 @@ function ConditionsTooltip({
|
||||
</Typography.Text>
|
||||
<div className={styles.keyList}>
|
||||
{resource.map((key) => (
|
||||
<code key={key} className={styles.key}>
|
||||
{key}
|
||||
<code key={`${key.origin}-${key.value}`} className={styles.key}>
|
||||
{key.value}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
SpantypesSpanMapperDTO as Mapper,
|
||||
SpantypesSpanMapperGroupDTO as MapperGroup,
|
||||
SpantypesSpanMapperOperationDTO as MapperOperation,
|
||||
SpantypesSpanMapperOriginDTO as MapperOrigin,
|
||||
SpantypesSpanMapperTestSpanDTO as TestSpan,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
@@ -21,9 +22,15 @@ export function makeGroup(overrides: Partial<MapperGroup> = {}): MapperGroup {
|
||||
orgId: 'org-1',
|
||||
name: 'demo',
|
||||
enabled: true,
|
||||
origin: MapperOrigin.user,
|
||||
version: 0,
|
||||
condition: {
|
||||
attributes: ['ai.embeddings'],
|
||||
resource: ['cloud.account.id'],
|
||||
attributes: [
|
||||
{ value: 'ai.embeddings', enabled: true, origin: MapperOrigin.user },
|
||||
],
|
||||
resource: [
|
||||
{ value: 'cloud.account.id', enabled: true, origin: MapperOrigin.user },
|
||||
],
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
@@ -35,6 +42,7 @@ export function makeMapper(overrides: Partial<Mapper> = {}): Mapper {
|
||||
groupId: 'group-1',
|
||||
name: 'gen_ai.request.model',
|
||||
enabled: true,
|
||||
origin: MapperOrigin.user,
|
||||
fieldContext: FieldContext.attribute,
|
||||
config: {
|
||||
sources: [
|
||||
@@ -43,12 +51,16 @@ export function makeMapper(overrides: Partial<Mapper> = {}): Mapper {
|
||||
context: FieldContext.attribute,
|
||||
operation: MapperOperation.copy,
|
||||
priority: 2,
|
||||
enabled: true,
|
||||
origin: MapperOrigin.user,
|
||||
},
|
||||
{
|
||||
key: 'llm.model',
|
||||
context: FieldContext.attribute,
|
||||
operation: MapperOperation.move,
|
||||
priority: 1,
|
||||
enabled: true,
|
||||
origin: MapperOrigin.user,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -85,8 +97,12 @@ export const mockGroups: MapperGroup[] = [
|
||||
id: 'group-1',
|
||||
name: 'demo',
|
||||
condition: {
|
||||
attributes: ['ai.embeddings'],
|
||||
resource: ['cloud.account.id'],
|
||||
attributes: [
|
||||
{ value: 'ai.embeddings', enabled: true, origin: MapperOrigin.user },
|
||||
],
|
||||
resource: [
|
||||
{ value: 'cloud.account.id', enabled: true, origin: MapperOrigin.user },
|
||||
],
|
||||
},
|
||||
}),
|
||||
makeGroup({
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Plus, X } from '@signozhq/icons';
|
||||
|
||||
import { FieldContextValue } from 'container/LLMObservability/AttributeMapping/types';
|
||||
import {
|
||||
ConditionKey,
|
||||
FieldContextValue,
|
||||
} from 'container/LLMObservability/AttributeMapping/types';
|
||||
import { createConditionKey } from 'container/LLMObservability/AttributeMapping/utils';
|
||||
import KeySearchInput from '../../../KeySearchInput/KeySearchInput';
|
||||
import styles from './ConditionKeyList.module.scss';
|
||||
|
||||
interface ConditionKeyListProps {
|
||||
label: string;
|
||||
labelHint?: string;
|
||||
keys: string[];
|
||||
keys: ConditionKey[];
|
||||
placeholder: string;
|
||||
addLabel: string;
|
||||
testIdPrefix: string;
|
||||
fieldContext: FieldContextValue;
|
||||
onChange: (keys: string[]) => void;
|
||||
onChange: (keys: ConditionKey[]) => void;
|
||||
}
|
||||
|
||||
function ConditionKeyList({
|
||||
@@ -27,11 +31,11 @@ function ConditionKeyList({
|
||||
onChange,
|
||||
}: ConditionKeyListProps): JSX.Element {
|
||||
const updateKey = (index: number, value: string): void => {
|
||||
onChange(keys.map((key, i) => (i === index ? value : key)));
|
||||
onChange(keys.map((key, i) => (i === index ? { ...key, value } : key)));
|
||||
};
|
||||
|
||||
const addKey = (): void => {
|
||||
onChange([...keys, '']);
|
||||
onChange([...keys, createConditionKey()]);
|
||||
};
|
||||
|
||||
const removeKey = (index: number): void => {
|
||||
@@ -53,7 +57,7 @@ function ConditionKeyList({
|
||||
<KeySearchInput
|
||||
className={styles.keyInput}
|
||||
placeholder={placeholder}
|
||||
value={key}
|
||||
value={key.value}
|
||||
fieldContext={fieldContext}
|
||||
onChange={(next): void => updateKey(index, next)}
|
||||
testId={`${testIdPrefix}-${index}`}
|
||||
|
||||
@@ -42,7 +42,9 @@ function sourcesEqual(a: SourceConfig[], b: SourceConfig[]): boolean {
|
||||
(source, index) =>
|
||||
source.key === b[index].key &&
|
||||
source.context === b[index].context &&
|
||||
source.operation === b[index].operation,
|
||||
source.operation === b[index].operation &&
|
||||
source.enabled === b[index].enabled &&
|
||||
source.origin === b[index].origin,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import {
|
||||
SpantypesFieldContextDTO,
|
||||
SpantypesSpanMapperDTO,
|
||||
SpantypesSpanMapperGroupConditionKeyDTO,
|
||||
SpantypesSpanMapperGroupDTO,
|
||||
SpantypesSpanMapperOperationDTO,
|
||||
SpantypesSpanMapperOriginDTO,
|
||||
SpantypesSpanMapperSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export type MapperGroup = SpantypesSpanMapperGroupDTO;
|
||||
@@ -11,21 +14,23 @@ export const FieldContext = SpantypesFieldContextDTO;
|
||||
export type FieldContextValue = SpantypesFieldContextDTO;
|
||||
export const MapperOperation = SpantypesSpanMapperOperationDTO;
|
||||
export type MapperOperationValue = SpantypesSpanMapperOperationDTO;
|
||||
export const MapperOrigin = SpantypesSpanMapperOriginDTO;
|
||||
export type MapperOriginValue = SpantypesSpanMapperOriginDTO;
|
||||
|
||||
export type ConditionKey = SpantypesSpanMapperGroupConditionKeyDTO;
|
||||
|
||||
export type MapperDraftMode = 'add' | 'edit';
|
||||
|
||||
export interface SourceConfig {
|
||||
key: string;
|
||||
context: SpantypesFieldContextDTO;
|
||||
operation: SpantypesSpanMapperOperationDTO;
|
||||
}
|
||||
// `priority` is left out: it is derived from list order when the draft is
|
||||
// serialized.
|
||||
export type SourceConfig = Omit<SpantypesSpanMapperSourceDTO, 'priority'>;
|
||||
|
||||
// Editable form state for a mapper. `sources` is ordered highest priority
|
||||
// first; `fieldContext` is where the standardized target is written.
|
||||
export interface MapperDraft {
|
||||
id: string | null;
|
||||
name: string;
|
||||
fieldContext: SpantypesFieldContextDTO;
|
||||
fieldContext: FieldContextValue;
|
||||
sources: SourceConfig[];
|
||||
enabled: boolean;
|
||||
}
|
||||
@@ -33,26 +38,20 @@ export interface MapperDraft {
|
||||
export interface GroupDraft {
|
||||
id: string | null;
|
||||
name: string;
|
||||
attributes: string[];
|
||||
resource: string[];
|
||||
attributes: ConditionKey[];
|
||||
resource: ConditionKey[];
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface DraftMapper {
|
||||
// The editor tree identifies rows by `localId` so unsaved ones are addressable;
|
||||
// `serverId` is null until the row has been persisted.
|
||||
export type DraftMapper = Omit<MapperDraft, 'id'> & {
|
||||
localId: string;
|
||||
serverId: string | null;
|
||||
name: string;
|
||||
fieldContext: SpantypesFieldContextDTO;
|
||||
sources: SourceConfig[];
|
||||
enabled: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export interface DraftGroup {
|
||||
export type DraftGroup = Omit<GroupDraft, 'id'> & {
|
||||
localId: string;
|
||||
serverId: string | null;
|
||||
name: string;
|
||||
attributes: string[];
|
||||
resource: string[];
|
||||
enabled: boolean;
|
||||
mappers: DraftMapper[];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import {
|
||||
SpantypesPostableSpanMapperDTO,
|
||||
SpantypesPostableSpanMapperGroupDTO,
|
||||
SpantypesSpanMapperGroupConditionKeyDTO,
|
||||
SpantypesUpdatableSpanMapperDTO,
|
||||
SpantypesUpdatableSpanMapperGroupDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import {
|
||||
ConditionKey,
|
||||
DraftGroup,
|
||||
DraftMapper,
|
||||
FieldContext,
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
MapperDraft,
|
||||
MapperGroup,
|
||||
MapperOperation,
|
||||
MapperOrigin,
|
||||
SourceConfig,
|
||||
} from './types';
|
||||
|
||||
@@ -24,20 +27,36 @@ function genLocalId(prefix: 'group' | 'mapper'): string {
|
||||
return `local-${prefix}-${uuid()}`;
|
||||
}
|
||||
|
||||
// Trimmed, de-duplicated, non-empty keys preserving input order.
|
||||
function cleanKeys(keys: string[]): string[] {
|
||||
export function createConditionKey(value = ''): ConditionKey {
|
||||
return { value, enabled: true, origin: MapperOrigin.user };
|
||||
}
|
||||
|
||||
// Trimmed, de-duplicated, non-empty keys preserving input order. A shipped and
|
||||
// a user key may share a value, so the origin is part of the identity.
|
||||
function cleanKeys(keys: ConditionKey[]): ConditionKey[] {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
const result: ConditionKey[] = [];
|
||||
keys.forEach((raw) => {
|
||||
const key = raw.trim();
|
||||
if (key && !seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(key);
|
||||
const value = raw.value.trim();
|
||||
const dedupeKey = `${raw.origin}:${value}`;
|
||||
if (value && !seen.has(dedupeKey)) {
|
||||
seen.add(dedupeKey);
|
||||
result.push({ ...raw, value });
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function fromConditionKeys(
|
||||
keys: SpantypesSpanMapperGroupConditionKeyDTO[] | null | undefined,
|
||||
): ConditionKey[] {
|
||||
return (keys ?? []).map((key) => ({
|
||||
value: key.value,
|
||||
enabled: key.enabled,
|
||||
origin: key.origin ?? MapperOrigin.user,
|
||||
}));
|
||||
}
|
||||
|
||||
// Source configs for a mapper, highest priority first (first match wins at
|
||||
// evaluation time).
|
||||
function getMapperSources(mapper: Mapper): SourceConfig[] {
|
||||
@@ -48,6 +67,8 @@ function getMapperSources(mapper: Mapper): SourceConfig[] {
|
||||
key: source.key,
|
||||
context: source.context,
|
||||
operation: source.operation,
|
||||
enabled: source.enabled,
|
||||
origin: source.origin ?? MapperOrigin.user,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -56,6 +77,8 @@ export function createEmptySource(): SourceConfig {
|
||||
key: '',
|
||||
context: FieldContext.attribute,
|
||||
operation: MapperOperation.copy,
|
||||
enabled: true,
|
||||
origin: MapperOrigin.user,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,7 +95,7 @@ function getCleanSources(draft: MapperDraft): SourceConfig[] {
|
||||
const result: SourceConfig[] = [];
|
||||
draft.sources.forEach((source) => {
|
||||
const key = source.key.trim();
|
||||
const dedupeKey = `${source.context}:${key}`;
|
||||
const dedupeKey = `${source.origin}:${source.context}:${key}`;
|
||||
if (key && !seen.has(dedupeKey)) {
|
||||
seen.add(dedupeKey);
|
||||
result.push({ ...source, key });
|
||||
@@ -95,6 +118,8 @@ function buildSources(
|
||||
context: source.context,
|
||||
operation: source.operation,
|
||||
priority: sources.length - index,
|
||||
enabled: source.enabled,
|
||||
origin: source.origin,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -123,7 +148,7 @@ export function buildUpdatableMapper(
|
||||
export const EMPTY_GROUP_DRAFT: GroupDraft = {
|
||||
id: null,
|
||||
name: '',
|
||||
attributes: [''],
|
||||
attributes: [createConditionKey()],
|
||||
resource: [],
|
||||
enabled: true,
|
||||
};
|
||||
@@ -170,8 +195,8 @@ export function buildDraftGroup(
|
||||
localId: group.id,
|
||||
serverId: group.id,
|
||||
name: group.name,
|
||||
attributes: group.condition?.attributes ?? [],
|
||||
resource: group.condition?.resource ?? [],
|
||||
attributes: fromConditionKeys(group.condition?.attributes),
|
||||
resource: fromConditionKeys(group.condition?.resource),
|
||||
enabled: group.enabled,
|
||||
mappers: mappers.map(buildDraftMapper),
|
||||
};
|
||||
@@ -182,7 +207,8 @@ export function groupDraftFromNode(group: DraftGroup): GroupDraft {
|
||||
return {
|
||||
id: group.localId,
|
||||
name: group.name,
|
||||
attributes: group.attributes.length > 0 ? group.attributes : [''],
|
||||
attributes:
|
||||
group.attributes.length > 0 ? group.attributes : [createConditionKey()],
|
||||
resource: group.resource,
|
||||
enabled: group.enabled,
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import cx from 'classnames';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
import { initialQueryAIWithType } from 'constants/queryBuilder';
|
||||
@@ -94,6 +95,13 @@ function Explorer(): JSX.Element {
|
||||
const [warning, setWarning] = useState<Warning | undefined>();
|
||||
const [isOpen, setOpen] = useState<boolean>(true);
|
||||
|
||||
const { startUnixMilli, endUnixMilli } = useSignalFieldApis();
|
||||
// existingQuery is left unset so related values auto-extract from the current query
|
||||
const quickFiltersFieldApis = useMemo(
|
||||
() => ({ startUnixMilli, endUnixMilli }),
|
||||
[startUnixMilli, endUnixMilli],
|
||||
);
|
||||
|
||||
const defaultQuery = useMemo(
|
||||
(): Query =>
|
||||
updateAllQueriesOperators(
|
||||
@@ -187,8 +195,9 @@ function Explorer(): JSX.Element {
|
||||
<Card className="filter" hidden={!isOpen}>
|
||||
<QuickFilters
|
||||
className="qf-traces-explorer"
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
signal={SignalType.TRACES}
|
||||
source={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
signal={SignalType.AI_OBSERVABILITY}
|
||||
useFieldApis={quickFiltersFieldApis}
|
||||
handleFilterVisibilityChange={(): void => {
|
||||
setOpen(!isOpen);
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TracesTableRow } from '../TracesTable/getFieldColumn';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { formUrlParams } from 'container/TraceDetail/utils';
|
||||
import { formUrlParams } from 'utils/traceUtils';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
|
||||
export const getTraceLink = (record: Record<string, unknown>): string => {
|
||||
@@ -14,7 +15,11 @@ export const getTraceLink = (record: Record<string, unknown>): string => {
|
||||
const traceId = readId(record.traceID) || readId(record.trace_id);
|
||||
const spanId = readId(record.spanID) || readId(record.span_id);
|
||||
|
||||
return `${ROUTES.TRACE}/${traceId}${formUrlParams({
|
||||
if (!traceId) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${generatePath(ROUTES.TRACE_DETAIL, { id: traceId })}${formUrlParams({
|
||||
spanId,
|
||||
levelUp: 0,
|
||||
levelDown: 0,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Badge } from '@signozhq/ui/badge';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||
import { getMs } from 'utils/timeUtils';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
import {
|
||||
|
||||
@@ -242,7 +242,7 @@ function Application(): JSX.Element {
|
||||
urlParams.set(QueryParams.startTime, startTime.toString());
|
||||
urlParams.set(QueryParams.endTime, endTime.toString());
|
||||
urlParams.delete(QueryParams.relativeTime);
|
||||
const avialableParams = routeConfig[ROUTES.TRACE];
|
||||
const avialableParams = routeConfig[ROUTES.TRACES_EXPLORER];
|
||||
const queryString = getQueryString(avialableParams, urlParams);
|
||||
|
||||
const JSONCompositeQuery = encodeURIComponent(
|
||||
|
||||
@@ -10,7 +10,7 @@ import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute
|
||||
import { AppState } from 'store/reducers';
|
||||
import { PayloadProps } from 'types/api/metrics/getTopOperations';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { Tags } from 'types/reducer/trace';
|
||||
import { Tags } from 'hooks/useResourceAttribute/types';
|
||||
|
||||
function TopOperation(): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
|
||||
@@ -10,7 +10,7 @@ import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||
import { resourceAttributesToTracesFilterItems } from 'hooks/useResourceAttribute/utils';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils';
|
||||
import { traceFilterKeys } from 'pages/TracesExplorer/Filter/filterUtils';
|
||||
import { traceFilterKeys } from 'constants/traceFilterKeys';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { Tags } from 'types/reducer/trace';
|
||||
import { Tags } from 'hooks/useResourceAttribute/types';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { secondsToMilliseconds } from 'utils/timeUtils';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -102,7 +102,7 @@ export function onViewTracePopupClick({
|
||||
urlParams.set(QueryParams.startTime, startTime.toString());
|
||||
urlParams.set(QueryParams.endTime, endTime.toString());
|
||||
urlParams.delete(QueryParams.relativeTime);
|
||||
const avialableParams = routeConfig[ROUTES.TRACE];
|
||||
const avialableParams = routeConfig[ROUTES.TRACES_EXPLORER];
|
||||
const queryString = getQueryString(avialableParams, urlParams);
|
||||
|
||||
const JSONCompositeQuery = encodeURIComponent(
|
||||
|
||||
@@ -56,10 +56,6 @@ jest.mock('../Tabs/util', () => ({
|
||||
}));
|
||||
|
||||
// Mock the resourceAttributesToTracesFilterItems function
|
||||
jest.mock('container/TraceDetail/utils', () => ({
|
||||
resourceAttributesToTracesFilterItems: (): any[] => [],
|
||||
}));
|
||||
|
||||
const mockedUseResourceAttribute = useResourceAttribute as jest.MockedFunction<
|
||||
typeof useResourceAttribute
|
||||
>;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.new-explorer-cta-with-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
export const buttonText: Record<string, string> = {
|
||||
[ROUTES.LOGS_EXPLORER]: 'Old Explorer',
|
||||
[ROUTES.TRACE]: 'New Explorer',
|
||||
[ROUTES.OLD_LOGS_EXPLORER]: 'New Explorer',
|
||||
[ROUTES.TRACES_EXPLORER]: 'Old Explorer',
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Button } from 'antd';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { Undo } from '@signozhq/icons';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import { buttonText } from './config';
|
||||
|
||||
import './NewExplorerCTA.styles.scss';
|
||||
|
||||
function NewExplorerCTA(): JSX.Element | null {
|
||||
const location = useLocation();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const isTraceOrLogsExplorerPage = useMemo(
|
||||
() =>
|
||||
location.pathname === ROUTES.LOGS_EXPLORER ||
|
||||
location.pathname === ROUTES.TRACE ||
|
||||
location.pathname === ROUTES.OLD_LOGS_EXPLORER ||
|
||||
location.pathname === ROUTES.TRACES_EXPLORER,
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const onClickHandler = useCallback(
|
||||
(e?: React.MouseEvent): void => {
|
||||
let targetPath: string;
|
||||
if (location.pathname === ROUTES.LOGS_EXPLORER) {
|
||||
targetPath = ROUTES.OLD_LOGS_EXPLORER;
|
||||
} else if (location.pathname === ROUTES.TRACE) {
|
||||
targetPath = ROUTES.TRACES_EXPLORER;
|
||||
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
|
||||
targetPath = ROUTES.LOGS_EXPLORER;
|
||||
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
|
||||
targetPath = ROUTES.TRACE;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
safeNavigate(targetPath, { newTab: !!e && isModifierKeyPressed(e) });
|
||||
},
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<Button
|
||||
icon={<Undo size={16} />}
|
||||
onClick={(e): void => onClickHandler(e)}
|
||||
data-testid="newExplorerCTA"
|
||||
type="text"
|
||||
className="periscope-btn link"
|
||||
>
|
||||
{buttonText[location.pathname]}
|
||||
</Button>
|
||||
),
|
||||
[location.pathname, onClickHandler],
|
||||
);
|
||||
|
||||
if (!isTraceOrLogsExplorerPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (location.pathname === ROUTES.TRACES_EXPLORER) {
|
||||
return button;
|
||||
}
|
||||
|
||||
if (location.pathname === ROUTES.LOGS_EXPLORER) {
|
||||
return button;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="new-explorer-cta-with-badge">
|
||||
{button}
|
||||
<Badge color="robin" variant="default">
|
||||
New
|
||||
</Badge>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewExplorerCTA;
|
||||
@@ -10,7 +10,7 @@ import useDebounce from 'hooks/useDebounce';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { has } from 'lodash-es';
|
||||
import { AllTraceFilterKeyValue } from 'pages/TracesExplorer/Filter/filterUtils';
|
||||
import { AllTraceFilterKeyValue } from 'constants/traceFilterKeys';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import {
|
||||
QueryKeyRequestProps,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button, Table, TableProps } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { StyledCSS } from 'container/GantChart/Trace/styles';
|
||||
import { StyledCSS } from 'types/styledComponents';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const FooterButton = styled(Button)`
|
||||
|
||||
@@ -11,7 +11,7 @@ import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { Tags } from 'types/reducer/trace';
|
||||
import { Tags } from 'hooks/useResourceAttribute/types';
|
||||
|
||||
import SkipOnBoardingModal from '../SkipOnBoardModal';
|
||||
import ServiceMetricsApplication from './ServiceMetricsApplication';
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { Tags } from 'types/reducer/trace';
|
||||
import { Tags } from 'hooks/useResourceAttribute/types';
|
||||
|
||||
import SkipOnBoardingModal from '../SkipOnBoardModal';
|
||||
import ServiceTraceTable from './ServiceTracesTable';
|
||||
|
||||
@@ -42,9 +42,7 @@ export const routeConfig: Record<string, QueryParams[]> = {
|
||||
[ROUTES.SIGN_UP]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.SOMETHING_WENT_WRONG]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.TRACES_EXPLORER]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.TRACE]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.TRACE_DETAIL]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.TRACE_DETAIL_OLD]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.UN_AUTHORIZED]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.USAGE_EXPLORER]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.VERSION]: [QueryParams.resourceAttributes],
|
||||
|
||||
@@ -568,7 +568,8 @@ export const getUserSettingsDropdownMenuItems = ({
|
||||
This is used to highlight the correct menu item when the user navigates to a new route
|
||||
**/
|
||||
export const NEW_ROUTES_MENU_ITEM_KEY_MAP: Record<string, string> = {
|
||||
[ROUTES.TRACE]: ROUTES.TRACES_EXPLORER,
|
||||
// Trace detail pages (/trace/:id) highlight the Traces menu item.
|
||||
'/trace': ROUTES.TRACES_EXPLORER,
|
||||
[ROUTES.TRACE_EXPLORER]: ROUTES.TRACES_EXPLORER,
|
||||
[ROUTES.LOGS_BASE]: ROUTES.LOGS_EXPLORER,
|
||||
[ROUTES.METRICS_EXPLORER_BASE]: ROUTES.METRICS_EXPLORER,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import LiveLogsPauseResume from 'container/LiveLogs/LiveLogsPauseResume/LiveLogsPauseResume';
|
||||
import NewExplorerCTA from 'container/NewExplorerCTA';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { noop } from 'lodash-es';
|
||||
|
||||
@@ -12,7 +11,6 @@ interface ToolbarProps {
|
||||
showAutoRefresh: boolean;
|
||||
leftActions?: JSX.Element;
|
||||
rightActions?: JSX.Element;
|
||||
showOldCTA?: boolean;
|
||||
warningElement?: JSX.Element;
|
||||
onGoLive?: () => void;
|
||||
onExitLiveLogs?: () => void;
|
||||
@@ -23,7 +21,6 @@ export default function Toolbar({
|
||||
showAutoRefresh,
|
||||
leftActions,
|
||||
rightActions,
|
||||
showOldCTA,
|
||||
warningElement,
|
||||
showLiveLogs,
|
||||
onGoLive,
|
||||
@@ -48,7 +45,6 @@ export default function Toolbar({
|
||||
<div className="rightActions">
|
||||
<div className="timeRange">
|
||||
{warningElement}
|
||||
{showOldCTA && <NewExplorerCTA />}
|
||||
{showLiveLogs && <LiveLogsPauseResume />}
|
||||
<DateTimeSelectionV2
|
||||
showLiveLogs={showLiveLogs}
|
||||
@@ -69,7 +65,6 @@ export default function Toolbar({
|
||||
Toolbar.defaultProps = {
|
||||
leftActions: <div />,
|
||||
rightActions: <div />,
|
||||
showOldCTA: false,
|
||||
warningElement: <div />,
|
||||
showLiveLogs: false,
|
||||
onGoLive: (): void => noop(),
|
||||
|
||||
@@ -20,11 +20,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/NewExplorerCTA', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
|
||||
@@ -20,11 +20,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/NewExplorerCTA', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
let mockOnCustomDateHandler: ((range: [unknown, unknown]) => void) | null =
|
||||
null;
|
||||
let mockOnValidCustomDateChange: ((data: { timeStr: string }) => void) | null =
|
||||
|
||||
@@ -61,11 +61,6 @@ jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('container/NewExplorerCTA', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
function NuqsParamSetter({ paramValue }: { paramValue: string }): JSX.Element {
|
||||
const [, setYAxisUnit] = useQueryState(
|
||||
'yAxisUnit',
|
||||
|
||||
@@ -15,11 +15,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/NewExplorerCTA', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="custom-time-picker" />,
|
||||
|
||||
@@ -20,11 +20,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/NewExplorerCTA', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
let mockOnCustomDateHandler: ((range: [unknown, unknown]) => void) | null =
|
||||
null;
|
||||
let mockOnValidCustomDateChange: ((data: { timeStr: string }) => void) | null =
|
||||
|
||||
@@ -143,7 +143,6 @@ export const routesToSkip = [
|
||||
ROUTES.SETTINGS,
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
ROUTES.TRACE_DETAIL,
|
||||
ROUTES.TRACE_DETAIL_OLD,
|
||||
ROUTES.ALL_CHANNELS,
|
||||
ROUTES.USAGE_EXPLORER,
|
||||
ROUTES.GET_STARTED_WITH_CLOUD,
|
||||
|
||||
@@ -12,7 +12,6 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import NewExplorerCTA from 'container/NewExplorerCTA';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import {
|
||||
useGlobalTimeQueryInvalidate,
|
||||
@@ -65,7 +64,6 @@ function DateTimeSelection({
|
||||
updateTimeInterval,
|
||||
globalTimeLoading,
|
||||
showResetButton = false,
|
||||
showOldExplorerCTA = false,
|
||||
defaultRelativeTime = RelativeTimeMap['6hr'] as Time,
|
||||
isModalTimeSelection = false,
|
||||
onTimeChange,
|
||||
@@ -688,12 +686,6 @@ function DateTimeSelection({
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
{showOldExplorerCTA && (
|
||||
<div style={{ marginRight: 12 }}>
|
||||
<NewExplorerCTA />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasSelectedTimeError && !refreshButtonHidden && showRefreshText && (
|
||||
<div className="refresh-text-container">
|
||||
<RefreshText
|
||||
@@ -770,7 +762,6 @@ interface DateTimeSelectionV2Props {
|
||||
showAutoRefresh: boolean;
|
||||
showRefreshText?: boolean;
|
||||
hideShareModal?: boolean;
|
||||
showOldExplorerCTA?: boolean;
|
||||
showResetButton?: boolean;
|
||||
defaultRelativeTime?: Time;
|
||||
isModalTimeSelection?: boolean;
|
||||
@@ -792,7 +783,6 @@ interface DateTimeSelectionV2Props {
|
||||
|
||||
DateTimeSelection.defaultProps = {
|
||||
hideShareModal: false,
|
||||
showOldExplorerCTA: false,
|
||||
showRefreshText: true,
|
||||
showResetButton: false,
|
||||
defaultRelativeTime: RelativeTimeMap['6hr'] as Time,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { matchPath, useHistory } from 'react-router-dom';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
import NewExplorerCTA from '../NewExplorerCTA';
|
||||
import DateTimeSelector from './DateTimeSelectionV2';
|
||||
import { routesToDisable, routesToSkip } from './DateTimeSelectionV2/constants';
|
||||
|
||||
@@ -44,7 +43,6 @@ function TopNav(): JSX.Element | null {
|
||||
|
||||
return !isRouteToSkip ? (
|
||||
<div className="top-nav-container">
|
||||
<NewExplorerCTA />
|
||||
<DateTimeSelector showAutoRefresh />
|
||||
<HeaderRightSection enableShare enableFeedback enableAnnouncements={false} />
|
||||
</div>
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import getFilters from 'api/trace/getFilters';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import { getFilter, updateURL } from 'store/actions/trace/util';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
|
||||
|
||||
import { CheckBoxContainer, ParaGraph } from './styles';
|
||||
|
||||
function CheckBoxComponent(props: CheckBoxProps): JSX.Element {
|
||||
const {
|
||||
selectedFilter,
|
||||
filterLoading,
|
||||
filterToFetchData,
|
||||
spansAggregate,
|
||||
selectedTags,
|
||||
filter,
|
||||
userSelectedFilter,
|
||||
isFilterExclude,
|
||||
spanKind,
|
||||
} = useSelector<AppState, TraceReducer>((state) => state.traces);
|
||||
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const { keyValue, name, value } = props;
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const isUserSelected =
|
||||
(userSelectedFilter.get(name) || []).find((e) => e === keyValue) !==
|
||||
undefined;
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const onCheckHandler = async (): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const newSelectedMap = new Map(selectedFilter);
|
||||
const preUserSelectedMap = new Map(userSelectedFilter);
|
||||
const preIsFilterExclude = new Map(isFilterExclude);
|
||||
|
||||
const isTopicPresent = preUserSelectedMap.get(name);
|
||||
|
||||
// append the value
|
||||
if (!isTopicPresent) {
|
||||
preUserSelectedMap.set(name, [keyValue]);
|
||||
} else {
|
||||
const isValuePresent =
|
||||
isTopicPresent.find((e) => e === keyValue) !== undefined;
|
||||
|
||||
// check the value if present then remove the value or isChecked
|
||||
if (isValuePresent) {
|
||||
preUserSelectedMap.set(
|
||||
name,
|
||||
isTopicPresent.filter((e) => e !== keyValue),
|
||||
);
|
||||
} else {
|
||||
// if not present add into the array of string
|
||||
preUserSelectedMap.set(name, [...isTopicPresent, keyValue]);
|
||||
}
|
||||
}
|
||||
|
||||
if (newSelectedMap.get(name)?.find((e) => e === keyValue)) {
|
||||
newSelectedMap.set(name, [
|
||||
...(newSelectedMap.get(name) || []).filter((e) => e !== keyValue),
|
||||
]);
|
||||
} else {
|
||||
newSelectedMap.set(name, [
|
||||
...new Set([...(newSelectedMap.get(name) || []), keyValue]),
|
||||
]);
|
||||
}
|
||||
|
||||
if (preIsFilterExclude.get(name) !== false) {
|
||||
preIsFilterExclude.set(name, true);
|
||||
}
|
||||
|
||||
const response = await getFilters({
|
||||
other: Object.fromEntries(newSelectedMap),
|
||||
end: String(globalTime.maxTime),
|
||||
start: String(globalTime.minTime),
|
||||
getFilters: filterToFetchData.filter((e) => e !== name),
|
||||
isFilterExclude: preIsFilterExclude,
|
||||
spanKind,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
const updatedFilter = getFilter(response.payload);
|
||||
|
||||
// updatedFilter.forEach((value, key) => {
|
||||
// if (key !== 'duration' && name !== key) {
|
||||
// preUserSelectedMap.set(key, Object.keys(value));
|
||||
// }
|
||||
|
||||
// if (key === 'duration') {
|
||||
// newSelectedMap.set('duration', [value.maxDuration, value.minDuration]);
|
||||
// }
|
||||
// });
|
||||
|
||||
updatedFilter.set(name, {
|
||||
[`${keyValue}`]: '-1',
|
||||
...(filter.get(name) || {}),
|
||||
...(updatedFilter.get(name) || {}),
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_ALL_FILTERS,
|
||||
payload: {
|
||||
selectedTags,
|
||||
current: spansAggregate.currentPage,
|
||||
filter: updatedFilter,
|
||||
filterToFetchData,
|
||||
selectedFilter: newSelectedMap,
|
||||
userSelected: preUserSelectedMap,
|
||||
isFilterExclude: preIsFilterExclude,
|
||||
order: spansAggregate.order,
|
||||
orderParam: spansAggregate.orderParam,
|
||||
pageSize: spansAggregate.pageSize,
|
||||
spanKind,
|
||||
},
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
updateURL(
|
||||
newSelectedMap,
|
||||
filterToFetchData,
|
||||
spansAggregate.currentPage,
|
||||
selectedTags,
|
||||
preIsFilterExclude,
|
||||
preUserSelectedMap,
|
||||
spansAggregate.order,
|
||||
spansAggregate.pageSize,
|
||||
spansAggregate.orderParam,
|
||||
);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
|
||||
notifications.error({
|
||||
message: response.error || 'Something went wrong',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: (error as AxiosError).toString() || 'Something went wrong',
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isCheckBoxSelected = isUserSelected;
|
||||
|
||||
const TooTipOverLay = useMemo(
|
||||
(): JSX.Element => <div>{keyValue}</div>,
|
||||
[keyValue],
|
||||
);
|
||||
|
||||
return (
|
||||
<CheckBoxContainer>
|
||||
<Checkbox
|
||||
disabled={isLoading || filterLoading}
|
||||
onClick={onCheckHandler}
|
||||
value={isCheckBoxSelected}
|
||||
key={keyValue}
|
||||
>
|
||||
<Tooltip overlay={TooTipOverLay}>
|
||||
<ParaGraph truncate={1}>{keyValue}</ParaGraph>
|
||||
</Tooltip>
|
||||
</Checkbox>
|
||||
{isCheckBoxSelected ? (
|
||||
<Typography>{value}</Typography>
|
||||
) : (
|
||||
<Typography>-</Typography>
|
||||
)}
|
||||
</CheckBoxContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface CheckBoxProps {
|
||||
keyValue: string;
|
||||
name: TraceFilterEnum;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default CheckBoxComponent;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const CheckBoxContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
`;
|
||||
|
||||
export const ParaGraph = styled(Typography.Text)`
|
||||
&&& {
|
||||
margin: 0;
|
||||
max-width: 8rem;
|
||||
}
|
||||
`;
|
||||
@@ -1,104 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Button, Input } from 'antd';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { INITIAL_FILTER_VALUE } from 'store/reducers/trace';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_SPAN_UPDATE_FILTER_DISPLAY_VALUE } from 'types/actions/trace';
|
||||
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
|
||||
|
||||
import CheckBoxComponent from '../Common/Checkbox';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
|
||||
const { filter, filterDisplayValue } = useSelector<AppState, TraceReducer>(
|
||||
(state) => state.traces,
|
||||
);
|
||||
|
||||
const { name } = props;
|
||||
|
||||
const status = filter.get(name) || {};
|
||||
|
||||
const statusObj = Object.keys(status);
|
||||
const numberOfFilters = filterDisplayValue.get(name) || 0;
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const [searchFilter, setSearchFilter] = useState<string>('');
|
||||
|
||||
const onClickMoreHandler = (): void => {
|
||||
const newFilterDisplayValue = new Map(filterDisplayValue);
|
||||
const preValue =
|
||||
(newFilterDisplayValue.get(name) || 0) + INITIAL_FILTER_VALUE;
|
||||
|
||||
newFilterDisplayValue.set(name, preValue);
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_SPAN_UPDATE_FILTER_DISPLAY_VALUE,
|
||||
payload: newFilterDisplayValue,
|
||||
});
|
||||
};
|
||||
|
||||
const isMoreButtonAvilable = Boolean(
|
||||
numberOfFilters && statusObj.length > numberOfFilters,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{statusObj.length > 0 && (
|
||||
<Search
|
||||
value={searchFilter}
|
||||
onChange={(e): void => setSearchFilter(e.target.value)}
|
||||
style={{
|
||||
padding: '0 3%',
|
||||
}}
|
||||
placeholder="Filter Values"
|
||||
/>
|
||||
)}
|
||||
|
||||
{statusObj
|
||||
.sort((a, b) => {
|
||||
const countA = +status[a];
|
||||
const countB = +status[b];
|
||||
|
||||
if (countA === countB) {
|
||||
return a.length - b.length;
|
||||
}
|
||||
return countA - countB;
|
||||
})
|
||||
.filter((filter) => {
|
||||
if (searchFilter.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return filter
|
||||
.toLocaleLowerCase()
|
||||
.includes(searchFilter.toLocaleLowerCase());
|
||||
})
|
||||
.filter((_, index) => index < numberOfFilters)
|
||||
.map((e) => (
|
||||
<CheckBoxComponent
|
||||
key={e}
|
||||
{...{
|
||||
name,
|
||||
keyValue: e,
|
||||
value: status[e],
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{isMoreButtonAvilable && (
|
||||
<Button onClick={onClickMoreHandler} type="link">
|
||||
More
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface CommonCheckBoxProps {
|
||||
name: TraceFilterEnum;
|
||||
}
|
||||
|
||||
export default CommonCheckBox;
|
||||
@@ -1,223 +0,0 @@
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Slider } from '@signozhq/ui/slider';
|
||||
import getFilters from 'api/trace/getFilters';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import { getFilter, updateURL } from 'store/actions/trace/util';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { TraceReducer } from 'types/reducer/trace';
|
||||
|
||||
import { Container, InputComponent, InputContainer, Text } from './styles';
|
||||
import { getMs } from './util';
|
||||
|
||||
function Duration(): JSX.Element {
|
||||
const {
|
||||
filter,
|
||||
selectedFilter,
|
||||
filterToFetchData,
|
||||
spansAggregate,
|
||||
selectedTags,
|
||||
userSelectedFilter,
|
||||
isFilterExclude,
|
||||
spanKind,
|
||||
} = useSelector<AppState, TraceReducer>((state) => state.traces);
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const preLocalMaxDuration = useRef<number>();
|
||||
const preLocalMinDuration = useRef<number>();
|
||||
|
||||
const getDuration = useMemo(() => {
|
||||
const selectedDuration = selectedFilter.get('duration');
|
||||
|
||||
if (selectedDuration) {
|
||||
return {
|
||||
maxDuration: selectedDuration[0],
|
||||
minDuration: selectedDuration[1],
|
||||
};
|
||||
}
|
||||
|
||||
return filter.get('duration') || {};
|
||||
}, [selectedFilter, filter]);
|
||||
|
||||
const [preMax, setPreMax] = useState<string>('');
|
||||
const [preMin, setPreMin] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const duration = getDuration || {};
|
||||
|
||||
const maxDuration = duration.maxDuration || '0';
|
||||
const minDuration = duration.minDuration || '0';
|
||||
|
||||
if (preLocalMaxDuration.current === undefined) {
|
||||
preLocalMaxDuration.current = parseFloat(maxDuration);
|
||||
}
|
||||
if (preLocalMinDuration.current === undefined) {
|
||||
preLocalMinDuration.current = parseFloat(minDuration);
|
||||
}
|
||||
|
||||
setPreMax(getMs(maxDuration));
|
||||
setPreMin(getMs(minDuration));
|
||||
}, [getDuration]);
|
||||
|
||||
const updatedUrl = async (min: number, max: number): Promise<void> => {
|
||||
const preSelectedFilter = new Map(selectedFilter);
|
||||
const preUserSelected = new Map(userSelectedFilter);
|
||||
|
||||
preSelectedFilter.set('duration', [
|
||||
String(max * 1000000),
|
||||
String(min * 1000000),
|
||||
]);
|
||||
|
||||
const response = await getFilters({
|
||||
end: String(globalTime.maxTime),
|
||||
getFilters: filterToFetchData,
|
||||
other: Object.fromEntries(preSelectedFilter),
|
||||
start: String(globalTime.minTime),
|
||||
isFilterExclude,
|
||||
spanKind,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
const preFilter = getFilter(response.payload);
|
||||
|
||||
preFilter.forEach((value, key) => {
|
||||
const values = Object.keys(value);
|
||||
if (key !== 'duration' && values.length) {
|
||||
preUserSelected.set(key, values);
|
||||
}
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_ALL_FILTERS,
|
||||
payload: {
|
||||
current: spansAggregate.currentPage,
|
||||
filter: preFilter,
|
||||
filterToFetchData,
|
||||
selectedFilter: preSelectedFilter,
|
||||
selectedTags,
|
||||
userSelected: preUserSelected,
|
||||
isFilterExclude,
|
||||
order: spansAggregate.order,
|
||||
pageSize: spansAggregate.pageSize,
|
||||
orderParam: spansAggregate.orderParam,
|
||||
spanKind,
|
||||
},
|
||||
});
|
||||
|
||||
updateURL(
|
||||
preSelectedFilter,
|
||||
filterToFetchData,
|
||||
spansAggregate.currentPage,
|
||||
selectedTags,
|
||||
isFilterExclude,
|
||||
userSelectedFilter,
|
||||
spansAggregate.order,
|
||||
spansAggregate.pageSize,
|
||||
spansAggregate.orderParam,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onRangeSliderHandler = (number: [string, string]): void => {
|
||||
const [min, max] = number;
|
||||
|
||||
setPreMin(min);
|
||||
setPreMax(max);
|
||||
};
|
||||
|
||||
const debouncedFunction = useDebouncedFn(
|
||||
(min, max) => {
|
||||
updatedUrl(min as number, max as number);
|
||||
},
|
||||
1500,
|
||||
undefined,
|
||||
);
|
||||
|
||||
const onChangeMaxHandler: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
const { value } = event.target;
|
||||
const min = preMin;
|
||||
const max = value;
|
||||
|
||||
onRangeSliderHandler([min, max]);
|
||||
debouncedFunction(min, max);
|
||||
};
|
||||
|
||||
const onChangeMinHandler: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
const { value } = event.target;
|
||||
const min = value;
|
||||
const max = preMax;
|
||||
|
||||
onRangeSliderHandler([min, max]);
|
||||
debouncedFunction(min, max);
|
||||
};
|
||||
|
||||
const onRangeHandler = (value: number | number[]): void => {
|
||||
const [min, max] = value as number[];
|
||||
updatedUrl(min, max);
|
||||
};
|
||||
|
||||
const TipComponent = useCallback(
|
||||
(value: number) => <div>{`${value.toString()}ms`}</div>,
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Container>
|
||||
<InputContainer>
|
||||
<Text>Min</Text>
|
||||
</InputContainer>
|
||||
<InputComponent
|
||||
addonAfter="ms"
|
||||
type="number"
|
||||
onChange={onChangeMinHandler}
|
||||
value={preMin}
|
||||
/>
|
||||
|
||||
<InputContainer>
|
||||
<Text>Max</Text>
|
||||
</InputContainer>
|
||||
<InputComponent
|
||||
addonAfter="ms"
|
||||
type="number"
|
||||
onChange={onChangeMaxHandler}
|
||||
value={preMax}
|
||||
/>
|
||||
</Container>
|
||||
|
||||
<Container>
|
||||
<Slider
|
||||
min={Number(getMs(String(preLocalMinDuration.current || 0)))}
|
||||
max={Number(getMs(String(preLocalMaxDuration.current || 0)))}
|
||||
range
|
||||
tooltip={{ formatter: TipComponent }}
|
||||
onChange={(value): void => {
|
||||
const [min, max] = value as number[];
|
||||
onRangeSliderHandler([String(min), String(max)]);
|
||||
}}
|
||||
onAfterChange={onRangeHandler}
|
||||
value={[Number(preMin), Number(preMax)]}
|
||||
/>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Duration;
|
||||
@@ -1,43 +0,0 @@
|
||||
// TODO(@signozhq/ui-input): migrate this styled(Input) once @signozhq/ui
|
||||
// Input supports `addonAfter` (the consumer renders `<InputComponent addonAfter="ms">`).
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Input } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const DurationText = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
min-height: 8vh;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const InputComponent = styled(Input)`
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
`;
|
||||
|
||||
export const InputContainer = styled.div`
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.2rem;
|
||||
`;
|
||||
|
||||
export const Text = styled(Typography)`
|
||||
&&& {
|
||||
text-align: left;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Container = styled.div`
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
`;
|
||||
@@ -1,13 +0,0 @@
|
||||
import dayjs from 'dayjs';
|
||||
import durationPlugin from 'dayjs/plugin/duration';
|
||||
|
||||
dayjs.extend(durationPlugin);
|
||||
|
||||
export const getMs = (value: string): string =>
|
||||
parseFloat(
|
||||
dayjs
|
||||
.duration({
|
||||
milliseconds: parseInt(value, 10) / 1000000,
|
||||
})
|
||||
.format('SSS'),
|
||||
).toFixed(2);
|
||||
@@ -1,134 +0,0 @@
|
||||
import { ChangeEvent, useEffect, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Input } from 'antd';
|
||||
import getFilters from 'api/trace/getFilters';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import { getFilter, updateURL } from 'store/actions/trace/util';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { TraceReducer } from 'types/reducer/trace';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
function TraceID(): JSX.Element {
|
||||
const {
|
||||
selectedFilter,
|
||||
filterToFetchData,
|
||||
spansAggregate,
|
||||
selectedTags,
|
||||
userSelectedFilter,
|
||||
isFilterExclude,
|
||||
spanKind,
|
||||
} = useSelector<AppState, TraceReducer>((state) => state.traces);
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [userEnteredValue, setUserEnteredValue] = useState<string>('');
|
||||
const { notifications } = useNotifications();
|
||||
useEffect(() => {
|
||||
setUserEnteredValue(selectedFilter.get('traceID')?.[0] || '');
|
||||
}, [selectedFilter]);
|
||||
const onSearch = async (value: string): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const preSelectedFilter = new Map(selectedFilter);
|
||||
const preUserSelected = new Map(userSelectedFilter);
|
||||
|
||||
if (value !== '') {
|
||||
preUserSelected.set('traceID', [value]);
|
||||
preSelectedFilter.set('traceID', [value]);
|
||||
} else {
|
||||
preUserSelected.delete('traceID');
|
||||
preSelectedFilter.delete('traceID');
|
||||
}
|
||||
const response = await getFilters({
|
||||
other: Object.fromEntries(preSelectedFilter),
|
||||
end: String(globalTime.maxTime),
|
||||
start: String(globalTime.minTime),
|
||||
getFilters: filterToFetchData,
|
||||
isFilterExclude,
|
||||
spanKind,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
const preFilter = getFilter(response.payload);
|
||||
preFilter.set('traceID', { traceID: value });
|
||||
preFilter.forEach((value, key) => {
|
||||
const values = Object.keys(value);
|
||||
if (key !== 'duration' && values.length) {
|
||||
preUserSelected.set(key, values);
|
||||
}
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_ALL_FILTERS,
|
||||
payload: {
|
||||
current: spansAggregate.currentPage,
|
||||
filter: preFilter,
|
||||
filterToFetchData,
|
||||
selectedFilter: preSelectedFilter,
|
||||
selectedTags,
|
||||
userSelected: preUserSelected,
|
||||
isFilterExclude,
|
||||
order: spansAggregate.order,
|
||||
pageSize: spansAggregate.pageSize,
|
||||
orderParam: spansAggregate.orderParam,
|
||||
spanKind,
|
||||
},
|
||||
});
|
||||
|
||||
updateURL(
|
||||
preSelectedFilter,
|
||||
filterToFetchData,
|
||||
spansAggregate.currentPage,
|
||||
selectedTags,
|
||||
isFilterExclude,
|
||||
userSelectedFilter,
|
||||
spansAggregate.order,
|
||||
spansAggregate.pageSize,
|
||||
spansAggregate.orderParam,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: (error as AxiosError).toString() || 'Something went wrong',
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
const onChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setUserEnteredValue(e.target.value);
|
||||
};
|
||||
const onBlur = (): void => {
|
||||
if (userEnteredValue !== selectedFilter.get('traceID')?.[0]) {
|
||||
onSearch(userEnteredValue);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Search
|
||||
placeholder="Filter by Trace ID"
|
||||
onSearch={onSearch}
|
||||
style={{
|
||||
marginBottom: '5rem',
|
||||
padding: '0 3%',
|
||||
}}
|
||||
loading={isLoading}
|
||||
value={userEnteredValue}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TraceID;
|
||||
@@ -1,43 +0,0 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Card } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
|
||||
|
||||
import CommonCheckBox from './CommonCheckBox';
|
||||
import Duration from './Duration';
|
||||
import TraceID from './SearchTraceID';
|
||||
|
||||
function PanelBody(props: PanelBodyProps): JSX.Element {
|
||||
const { type } = props;
|
||||
|
||||
const { filterLoading } = useSelector<AppState, TraceReducer>(
|
||||
(state) => state.traces,
|
||||
);
|
||||
|
||||
if (filterLoading) {
|
||||
return (
|
||||
<Card bordered={false}>
|
||||
<Spinner height="10vh" tip="Loading.." />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
const renderBody = (type: TraceFilterEnum): JSX.Element => {
|
||||
switch (type) {
|
||||
case 'traceID':
|
||||
return <TraceID />;
|
||||
case 'duration':
|
||||
return <Duration />;
|
||||
default:
|
||||
return <CommonCheckBox name={type} />;
|
||||
}
|
||||
};
|
||||
return <Card bordered={false}>{renderBody(type)}</Card>;
|
||||
}
|
||||
|
||||
interface PanelBodyProps {
|
||||
type: TraceFilterEnum;
|
||||
}
|
||||
|
||||
export default PanelBody;
|
||||
@@ -1,360 +0,0 @@
|
||||
import { MouseEventHandler, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
import { Card } from 'antd';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import getFilters from 'api/trace/getFilters';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import { getFilter, updateURL } from 'store/actions/trace/util';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import {
|
||||
AllPanelHeading,
|
||||
TraceFilterEnum,
|
||||
TraceReducer,
|
||||
} from 'types/reducer/trace';
|
||||
|
||||
import {
|
||||
ButtonComponent,
|
||||
ButtonContainer,
|
||||
Container,
|
||||
IconContainer,
|
||||
TextContainer,
|
||||
} from './styles';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function PanelHeading(props: PanelHeadingProps): JSX.Element {
|
||||
const {
|
||||
filterLoading,
|
||||
filterToFetchData,
|
||||
selectedFilter,
|
||||
spansAggregate,
|
||||
selectedTags,
|
||||
filter,
|
||||
isFilterExclude,
|
||||
userSelectedFilter,
|
||||
spanKind,
|
||||
} = useSelector<AppState, TraceReducer>((state) => state.traces);
|
||||
|
||||
const { name: PanelName, isOpen: IsPanelOpen } = props;
|
||||
|
||||
const isDefaultOpen =
|
||||
filterToFetchData.find((e) => e === PanelName) !== undefined;
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const global = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const defaultErrorMessage = 'Something went wrong';
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const onExpandHandler: MouseEventHandler<HTMLDivElement> = async (e) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
setIsLoading(true);
|
||||
let updatedFilterData: TraceReducer['filterToFetchData'] = [];
|
||||
const getprepdatedSelectedFilter = new Map(selectedFilter);
|
||||
const getPreUserSelected = new Map(userSelectedFilter);
|
||||
|
||||
updatedFilterData = [PanelName];
|
||||
|
||||
const response = await getFilters({
|
||||
end: String(global.maxTime),
|
||||
start: String(global.minTime),
|
||||
getFilters: updatedFilterData,
|
||||
other: Object.fromEntries(getprepdatedSelectedFilter),
|
||||
isFilterExclude,
|
||||
spanKind,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
const updatedFilter = getFilter(response.payload);
|
||||
|
||||
if (!getPreUserSelected.has(PanelName)) {
|
||||
getPreUserSelected.set(
|
||||
PanelName,
|
||||
Object.keys(updatedFilter.get(PanelName) || []),
|
||||
);
|
||||
}
|
||||
|
||||
updatedFilterData = [...filterToFetchData, PanelName];
|
||||
filter.forEach((value, key) => {
|
||||
if (key !== PanelName) {
|
||||
updatedFilter.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_ALL_FILTERS,
|
||||
payload: {
|
||||
current: spansAggregate.currentPage,
|
||||
filter: updatedFilter,
|
||||
filterToFetchData: updatedFilterData,
|
||||
selectedFilter: getprepdatedSelectedFilter,
|
||||
selectedTags,
|
||||
userSelected: getPreUserSelected,
|
||||
isFilterExclude,
|
||||
order: spansAggregate.order,
|
||||
pageSize: spansAggregate.pageSize,
|
||||
orderParam: spansAggregate.orderParam,
|
||||
spanKind,
|
||||
},
|
||||
});
|
||||
|
||||
updateURL(
|
||||
getprepdatedSelectedFilter,
|
||||
updatedFilterData,
|
||||
spansAggregate.currentPage,
|
||||
selectedTags,
|
||||
isFilterExclude,
|
||||
getPreUserSelected,
|
||||
spansAggregate.order,
|
||||
spansAggregate.pageSize,
|
||||
spansAggregate.orderParam,
|
||||
);
|
||||
} else {
|
||||
notifications.error({
|
||||
message: response.error || defaultErrorMessage,
|
||||
});
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: (error as AxiosError).toString() || defaultErrorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function removes the selected filter
|
||||
*/
|
||||
const onCloseHandler = (): void => {
|
||||
const preSelectedFilter = new Map(selectedFilter);
|
||||
// removing the filter from filter to fetch the data
|
||||
const preFilterToFetchTheData = [
|
||||
...filterToFetchData.filter((name) => name !== PanelName),
|
||||
];
|
||||
|
||||
// preSelectedFilter.delete(PanelName);
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_ALL_FILTERS,
|
||||
payload: {
|
||||
current: spansAggregate.currentPage,
|
||||
filter,
|
||||
filterToFetchData: preFilterToFetchTheData,
|
||||
selectedFilter: preSelectedFilter,
|
||||
selectedTags,
|
||||
userSelected: userSelectedFilter,
|
||||
isFilterExclude,
|
||||
order: spansAggregate.order,
|
||||
pageSize: spansAggregate.pageSize,
|
||||
orderParam: spansAggregate.orderParam,
|
||||
spanKind,
|
||||
},
|
||||
});
|
||||
|
||||
updateURL(
|
||||
preSelectedFilter,
|
||||
preFilterToFetchTheData,
|
||||
spansAggregate.currentPage,
|
||||
selectedTags,
|
||||
isFilterExclude,
|
||||
userSelectedFilter,
|
||||
spansAggregate.order,
|
||||
spansAggregate.pageSize,
|
||||
spansAggregate.orderParam,
|
||||
);
|
||||
};
|
||||
|
||||
const onClearAllHandler = async (): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const updatedFilter = new Map(selectedFilter);
|
||||
const preUserSelected = new Map(userSelectedFilter);
|
||||
|
||||
updatedFilter.delete(PanelName);
|
||||
preUserSelected.delete(PanelName);
|
||||
|
||||
const postIsFilterExclude = new Map(isFilterExclude);
|
||||
|
||||
postIsFilterExclude.set(PanelName, false);
|
||||
|
||||
const response = await getFilters({
|
||||
end: String(global.maxTime),
|
||||
start: String(global.minTime),
|
||||
getFilters: filterToFetchData,
|
||||
other: Object.fromEntries(updatedFilter),
|
||||
isFilterExclude: postIsFilterExclude,
|
||||
spanKind,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200 && response.payload) {
|
||||
const getUpdatedFilter = getFilter(response.payload);
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_ALL_FILTERS,
|
||||
payload: {
|
||||
current: spansAggregate.currentPage,
|
||||
filter: getUpdatedFilter,
|
||||
filterToFetchData,
|
||||
selectedFilter: updatedFilter,
|
||||
selectedTags,
|
||||
userSelected: preUserSelected,
|
||||
isFilterExclude: postIsFilterExclude,
|
||||
order: spansAggregate.order,
|
||||
pageSize: spansAggregate.pageSize,
|
||||
orderParam: spansAggregate.orderParam,
|
||||
spanKind,
|
||||
},
|
||||
});
|
||||
|
||||
updateURL(
|
||||
updatedFilter,
|
||||
filterToFetchData,
|
||||
spansAggregate.currentPage,
|
||||
selectedTags,
|
||||
postIsFilterExclude,
|
||||
preUserSelected,
|
||||
spansAggregate.order,
|
||||
spansAggregate.pageSize,
|
||||
spansAggregate.orderParam,
|
||||
);
|
||||
} else {
|
||||
notifications.error({
|
||||
message: response.error || 'Something went wrong',
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: (error as AxiosError).toString(),
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// const onSelectAllHandler = async () => {
|
||||
// try {
|
||||
// setIsLoading(true);
|
||||
// const preFilter = new Map(filter);
|
||||
// const preSelectedFilter = new Map(selectedFilter);
|
||||
|
||||
// preSelectedFilter.set(
|
||||
// props.name,
|
||||
// Object.keys(preFilter.get(props.name) || {}),
|
||||
// );
|
||||
|
||||
// const response = await getFilters({
|
||||
// end: String(global.maxTime),
|
||||
// start: String(global.minTime),
|
||||
// getFilters: filterToFetchData,
|
||||
// other: Object.fromEntries(preSelectedFilter),
|
||||
// });
|
||||
|
||||
// if (response.statusCode === 200 && response.payload) {
|
||||
// const getUpatedFilter = getFilter(response.payload);
|
||||
|
||||
// preSelectedFilter.set(
|
||||
// props.name,
|
||||
// Object.keys(getUpatedFilter.get(props.name) || {}),
|
||||
// );
|
||||
|
||||
// dispatch({
|
||||
// type: UPDATE_ALL_FILTERS,
|
||||
// payload: {
|
||||
// current: spansAggregate.currentPage,
|
||||
// filter: preFilter,
|
||||
// filterToFetchData,
|
||||
// selectedFilter: preSelectedFilter,
|
||||
// selectedTags,
|
||||
// },
|
||||
// });
|
||||
|
||||
// updateURL(
|
||||
// preSelectedFilter,
|
||||
// filterToFetchData,
|
||||
// spansAggregate.currentPage,
|
||||
// selectedTags,
|
||||
// preFilter,
|
||||
// );
|
||||
// }
|
||||
// setIsLoading(false);
|
||||
// } catch (error) {
|
||||
// setIsLoading(false);
|
||||
|
||||
// notification.error({
|
||||
// message: (error as AxiosError).toString(),
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
return (
|
||||
<>
|
||||
{PanelName !== 'duration' && <Divider plain style={{ margin: 0 }} />}
|
||||
|
||||
<Card bordered={false}>
|
||||
<Container
|
||||
disabled={filterLoading || isLoading}
|
||||
aria-disabled={filterLoading || isLoading}
|
||||
aria-expanded={IsPanelOpen}
|
||||
>
|
||||
<TextContainer onClick={isDefaultOpen ? onCloseHandler : onExpandHandler}>
|
||||
<IconContainer>
|
||||
{!IsPanelOpen ? <ChevronRight size="lg" /> : <ChevronDown size="lg" />}
|
||||
</IconContainer>
|
||||
|
||||
<Text style={{ textTransform: 'capitalize' }} truncate={1}>
|
||||
{AllPanelHeading.find((e) => e.key === PanelName)?.displayValue || ''}
|
||||
</Text>
|
||||
</TextContainer>
|
||||
|
||||
{PanelName !== 'duration' && (
|
||||
<ButtonContainer>
|
||||
{/* <ButtonComponent
|
||||
aria-disabled={isLoading || filterLoading}
|
||||
disabled={isLoading || filterLoading}
|
||||
onClick={onSelectAllHandler}
|
||||
type="link"
|
||||
>
|
||||
Select All
|
||||
</ButtonComponent> */}
|
||||
|
||||
<ButtonComponent
|
||||
aria-disabled={isLoading || filterLoading}
|
||||
disabled={isLoading || filterLoading}
|
||||
onClick={onClearAllHandler}
|
||||
type="link"
|
||||
>
|
||||
Clear All
|
||||
</ButtonComponent>
|
||||
</ButtonContainer>
|
||||
)}
|
||||
</Container>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface PanelHeadingProps {
|
||||
name: TraceFilterEnum;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default PanelHeading;
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Button } from 'antd';
|
||||
import { StyledCSS } from 'container/GantChart/Trace/styles';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
interface Props {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export const Container = styled.div<Props>`
|
||||
&&& {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-left: 0.5rem;
|
||||
min-height: 5vh;
|
||||
|
||||
cursor: ${({ disabled }): string => (disabled ? 'not-allowed' : '')};
|
||||
|
||||
${({ disabled }): StyledCSS =>
|
||||
disabled &&
|
||||
css`
|
||||
opacity: 0.5;
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
export const IconContainer = styled.div`
|
||||
&&& {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TextContainer = styled.div`
|
||||
&&& {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ButtonComponent = styled(Button)`
|
||||
&&& {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ButtonContainer = styled.div`
|
||||
&&& {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
@@ -1,30 +0,0 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
|
||||
|
||||
import PanelBody from './PanelBody';
|
||||
import PanelHeading from './PanelHeading';
|
||||
|
||||
function Panel(props: PanelProps): JSX.Element {
|
||||
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
|
||||
|
||||
const { name } = props;
|
||||
|
||||
const isDefaultOpen =
|
||||
traces.filterToFetchData.find((e) => e === name) !== undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelHeading name={name} isOpen={isDefaultOpen} />
|
||||
|
||||
{isDefaultOpen && <PanelBody type={name} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface PanelProps {
|
||||
name: TraceFilterEnum;
|
||||
}
|
||||
|
||||
export default Panel;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { TraceFilterEnum } from 'types/reducer/trace';
|
||||
|
||||
import Panel from './Panel';
|
||||
|
||||
export const AllTraceFilterEnum: TraceFilterEnum[] = [
|
||||
'duration',
|
||||
'status',
|
||||
'serviceName',
|
||||
'operation',
|
||||
'rpcMethod',
|
||||
'responseStatusCode',
|
||||
'httpHost',
|
||||
'httpMethod',
|
||||
'httpRoute',
|
||||
'httpUrl',
|
||||
'traceID',
|
||||
];
|
||||
|
||||
function Filters(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{AllTraceFilterEnum.map((panelName) => (
|
||||
<Panel key={panelName} name={panelName} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Filters;
|
||||
@@ -1,126 +0,0 @@
|
||||
import { ChartData, ChartDatasetProperties } from 'chart.js';
|
||||
import dayjs from 'dayjs';
|
||||
import { colors } from 'lib/getRandomColor';
|
||||
import { keys } from 'lodash-es';
|
||||
import { TraceReducer } from 'types/reducer/trace';
|
||||
|
||||
function transposeArray(array: number[][], arrayLength: number): number[][] {
|
||||
const newArray: number[][] = [];
|
||||
for (let i = 0; i < array.length; i += 1) {
|
||||
newArray.push([]);
|
||||
}
|
||||
|
||||
for (let i = 0; i < array.length; i += 1) {
|
||||
for (let j = 0; j < arrayLength; j += 1) {
|
||||
newArray[j]?.push(array[i][j]);
|
||||
}
|
||||
}
|
||||
|
||||
return newArray;
|
||||
}
|
||||
|
||||
export const getChartData = (
|
||||
data: TraceReducer['spansGraph']['payload'],
|
||||
): ChartData<'line'> => {
|
||||
const allDataPoints = data.items;
|
||||
|
||||
const chartDataset: ChartDatasetProperties<'line', number[]> = {
|
||||
data: [],
|
||||
type: 'line',
|
||||
};
|
||||
const chartLabels: ChartData<'line'>['labels'] = [];
|
||||
|
||||
if (allDataPoints && typeof allDataPoints === 'object') {
|
||||
Object.keys(allDataPoints).forEach((timestamp) => {
|
||||
const key = allDataPoints[timestamp];
|
||||
if (key.value) {
|
||||
chartDataset.data.push(key.value);
|
||||
const date = dayjs(key.timestamp / 1000000);
|
||||
chartLabels.push(date.toDate().getTime());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
...chartDataset,
|
||||
borderWidth: 1.5,
|
||||
spanGaps: true,
|
||||
borderColor: colors[0] || 'red',
|
||||
showLine: true,
|
||||
pointRadius: 0,
|
||||
},
|
||||
],
|
||||
labels: chartLabels,
|
||||
};
|
||||
};
|
||||
|
||||
export const getChartDataforGroupBy = (
|
||||
props: TraceReducer['spansGraph']['payload'],
|
||||
): ChartData => {
|
||||
const { items } = props;
|
||||
|
||||
const chartData: ChartData = {
|
||||
datasets: [],
|
||||
labels: [],
|
||||
};
|
||||
|
||||
let max = 0;
|
||||
|
||||
const allGroupBy = Object.keys(items).map((e) => items[e].groupBy);
|
||||
|
||||
keys(allGroupBy).forEach((e: string): void => {
|
||||
// @ts-expect-error
|
||||
const { length } = keys(allGroupBy[e]);
|
||||
|
||||
if (length >= max) {
|
||||
max = length;
|
||||
}
|
||||
});
|
||||
|
||||
const numberOfGraphs = max;
|
||||
|
||||
const spansGraph: number[][] = [];
|
||||
|
||||
const names: string[] = [];
|
||||
|
||||
// number of data points
|
||||
Object.keys(items).forEach((item) => {
|
||||
const spanData = items[item];
|
||||
const date = dayjs(Number(item) / 1000000)
|
||||
.toDate()
|
||||
.getTime();
|
||||
|
||||
chartData.labels?.push(date);
|
||||
|
||||
const { groupBy } = spanData;
|
||||
const preData: number[] = [];
|
||||
|
||||
if (groupBy) {
|
||||
Object.keys(groupBy).forEach((key) => {
|
||||
const value = groupBy[key];
|
||||
preData.push(value);
|
||||
names.push(key);
|
||||
});
|
||||
|
||||
spansGraph.push(preData);
|
||||
}
|
||||
});
|
||||
|
||||
const updatedName = [...new Set(names)];
|
||||
|
||||
transposeArray(spansGraph, numberOfGraphs).forEach((values, index) => {
|
||||
chartData.datasets.push({
|
||||
data: values.map((e) => e || 0),
|
||||
borderWidth: 1.5,
|
||||
spanGaps: true,
|
||||
borderColor: colors[index] || 'red',
|
||||
showLine: true,
|
||||
pointRadius: 0,
|
||||
label: updatedName[index],
|
||||
});
|
||||
});
|
||||
|
||||
return chartData;
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useMeasure } from 'react-use';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import Graph from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { TraceReducer } from 'types/reducer/trace';
|
||||
|
||||
import { getChartData, getChartDataforGroupBy } from './config';
|
||||
import { Container } from './styles';
|
||||
|
||||
function TraceGraph(): JSX.Element {
|
||||
const [ref, { width }] = useMeasure();
|
||||
|
||||
const { spansGraph, selectedGroupBy, yAxisUnit } = useSelector<
|
||||
AppState,
|
||||
TraceReducer
|
||||
>((state) => state.traces);
|
||||
|
||||
const { loading, error, errorMessage, payload } = spansGraph;
|
||||
|
||||
const ChartData = useMemo(
|
||||
() =>
|
||||
selectedGroupBy.length === 0 || selectedGroupBy === 'none'
|
||||
? getChartData(payload)
|
||||
: getChartDataforGroupBy(payload),
|
||||
[payload, selectedGroupBy],
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Container center>
|
||||
<Typography>{errorMessage || 'Something went wrong'}</Typography>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Container>
|
||||
<Spinner height="20vh" size="small" tip="Loading..." />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container ref={ref as never}>
|
||||
<Graph
|
||||
animate={false}
|
||||
data={ChartData}
|
||||
name="traceGraph"
|
||||
type="line"
|
||||
yAxisUnit={yAxisUnit}
|
||||
forceReRender={width}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default TraceGraph;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { RefObject } from 'react';
|
||||
import { StyledCSS } from 'container/GantChart/Trace/styles';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
interface Props {
|
||||
center?: boolean;
|
||||
ref?: RefObject<HTMLDivElement> | null; // The ref type provided by react-use is incorrect -> https://github.com/streamich/react-use/issues/1264 Open Issue
|
||||
}
|
||||
|
||||
export const Container = styled.div<Props>`
|
||||
height: 25vh !important;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
width: 100% !important;
|
||||
|
||||
${({ center }): StyledCSS =>
|
||||
center &&
|
||||
css`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`}
|
||||
`;
|
||||
@@ -1,97 +0,0 @@
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { AutoComplete } from 'antd';
|
||||
import getTagFilters from 'api/trace/getTagFilter';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { TraceReducer } from 'types/reducer/trace';
|
||||
|
||||
import { getTagKeyOptions, onTagKeySelect } from './utils';
|
||||
|
||||
function TagsKey(props: TagsKeysProps): JSX.Element {
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const { index, setLocalSelectedTags, tag } = props;
|
||||
|
||||
const [selectedKey, setSelectedKey] = useState<string>(tag.Key || '');
|
||||
|
||||
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
|
||||
|
||||
const { isLoading, data } = useQuery(
|
||||
[
|
||||
'getTagKeys',
|
||||
globalTime.minTime,
|
||||
globalTime.maxTime,
|
||||
traces.selectedFilter,
|
||||
traces.isFilterExclude,
|
||||
traces.spanKind,
|
||||
],
|
||||
{
|
||||
queryFn: () =>
|
||||
getTagFilters({
|
||||
start: globalTime.minTime,
|
||||
end: globalTime.maxTime,
|
||||
other: Object.fromEntries(traces.selectedFilter),
|
||||
isFilterExclude: traces.isFilterExclude,
|
||||
spanKind: traces.spanKind,
|
||||
}),
|
||||
cacheTime: 120000,
|
||||
},
|
||||
);
|
||||
|
||||
const options = useMemo(() => getTagKeyOptions(data?.payload), [data]);
|
||||
|
||||
const onSelectHandler = useCallback(
|
||||
(value: unknown) =>
|
||||
onTagKeySelect(
|
||||
value,
|
||||
options,
|
||||
setSelectedKey,
|
||||
setLocalSelectedTags,
|
||||
index,
|
||||
tag,
|
||||
),
|
||||
[index, options, setLocalSelectedTags, tag],
|
||||
);
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
style={{ width: '100%' }}
|
||||
value={selectedKey}
|
||||
allowClear
|
||||
disabled={isLoading}
|
||||
notFoundContent="No tags available"
|
||||
showSearch
|
||||
options={options?.map((e) => ({
|
||||
label: e.label?.toString(),
|
||||
value: e.value,
|
||||
}))}
|
||||
filterOption={(inputValue, option): boolean =>
|
||||
option?.label?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
|
||||
}
|
||||
onChange={(e): void => setSelectedKey(e)}
|
||||
onSelect={onSelectHandler}
|
||||
>
|
||||
<Input placeholder="Please select" />
|
||||
</AutoComplete>
|
||||
);
|
||||
}
|
||||
|
||||
interface TagsKeysProps {
|
||||
index: number;
|
||||
tag: FlatArray<TraceReducer['selectedTags'], 1>;
|
||||
setLocalSelectedTags: Dispatch<SetStateAction<TraceReducer['selectedTags']>>;
|
||||
}
|
||||
|
||||
export default TagsKey;
|
||||
@@ -1,154 +0,0 @@
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Select } from 'antd';
|
||||
import type { BaseOptionType } from 'antd/es/select';
|
||||
import getTagValue from 'api/trace/getTagValue';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { TraceReducer } from 'types/reducer/trace';
|
||||
|
||||
import { SelectComponent } from './styles';
|
||||
import {
|
||||
disableTagValue,
|
||||
extractTagKey,
|
||||
extractTagType,
|
||||
getInitialLocalValue,
|
||||
getTagValueOptions,
|
||||
onTagValueChange,
|
||||
selectOptions,
|
||||
separateTagValues,
|
||||
TagValueTypes,
|
||||
} from './utils';
|
||||
|
||||
function TagValue(props: TagValueProps): JSX.Element {
|
||||
const { tag, setLocalSelectedTags, index, tagKey } = props;
|
||||
const {
|
||||
Key: selectedKey,
|
||||
Operator: selectedOperator,
|
||||
StringValues: selectedStringValues,
|
||||
NumberValues: selectedNumberValues,
|
||||
BoolValues: selectedBoolValues,
|
||||
} = tag;
|
||||
|
||||
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
|
||||
|
||||
const [localTagValue, setLocalTagValue] = useState<TagValueTypes[]>(
|
||||
getInitialLocalValue(
|
||||
selectedNumberValues,
|
||||
selectedBoolValues,
|
||||
selectedStringValues,
|
||||
),
|
||||
);
|
||||
|
||||
const globalReducer = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const tagType = useMemo(() => extractTagType(tagKey), [tagKey]);
|
||||
|
||||
const { isLoading, data } = useQuery(
|
||||
[
|
||||
'tagKey',
|
||||
globalReducer.minTime,
|
||||
globalReducer.maxTime,
|
||||
tagKey,
|
||||
tagType,
|
||||
traces.spanKind,
|
||||
],
|
||||
{
|
||||
queryFn: () =>
|
||||
getTagValue({
|
||||
end: globalReducer.maxTime,
|
||||
start: globalReducer.minTime,
|
||||
tagKey: {
|
||||
Key: extractTagKey(tagKey),
|
||||
Type: tagType,
|
||||
},
|
||||
spanKind: traces.spanKind,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const tagValueDisabled = useMemo(
|
||||
() =>
|
||||
disableTagValue(
|
||||
selectedOperator,
|
||||
setLocalTagValue,
|
||||
selectedKey,
|
||||
setLocalSelectedTags,
|
||||
index,
|
||||
),
|
||||
[index, selectedKey, selectedOperator, setLocalSelectedTags],
|
||||
);
|
||||
|
||||
const onChangeHandler = useCallback(
|
||||
(value: unknown) => {
|
||||
const updatedValues = onTagValueChange(value);
|
||||
setLocalTagValue(updatedValues);
|
||||
const { boolValues, numberValues, stringValues } = separateTagValues(
|
||||
updatedValues,
|
||||
selectedKey,
|
||||
);
|
||||
|
||||
setLocalSelectedTags((tags) => [
|
||||
...tags.slice(0, index),
|
||||
{
|
||||
...tags[index],
|
||||
BoolValues: boolValues,
|
||||
NumberValues: numberValues,
|
||||
StringValues: stringValues,
|
||||
},
|
||||
...tags.slice(index + 1),
|
||||
]);
|
||||
},
|
||||
[index, setLocalSelectedTags, selectedKey],
|
||||
);
|
||||
|
||||
const getFilterOptions = useCallback(
|
||||
(inputValue: string, option?: BaseOptionType): boolean => {
|
||||
if (typeof option?.label === 'string') {
|
||||
return option?.label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<SelectComponent
|
||||
loading={isLoading}
|
||||
options={getTagValueOptions(data?.payload, tagType)}
|
||||
mode="tags"
|
||||
allowClear
|
||||
showSearch
|
||||
filterOption={getFilterOptions}
|
||||
disabled={isLoading || tagValueDisabled}
|
||||
value={localTagValue}
|
||||
onChange={onChangeHandler}
|
||||
>
|
||||
{selectOptions(data?.payload, tagType)?.map((suggestion) => (
|
||||
<Select.Option key={suggestion.toString()} value={suggestion}>
|
||||
{suggestion}
|
||||
</Select.Option>
|
||||
))}
|
||||
</SelectComponent>
|
||||
);
|
||||
}
|
||||
|
||||
interface TagValueProps {
|
||||
index: number;
|
||||
tag: FlatArray<TraceReducer['selectedTags'], 1>;
|
||||
setLocalSelectedTags: Dispatch<SetStateAction<TraceReducer['selectedTags']>>;
|
||||
tagKey: string;
|
||||
}
|
||||
|
||||
export default memo(TagValue);
|
||||
@@ -1,180 +0,0 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { X } from '@signozhq/icons';
|
||||
import { Select } from 'antd';
|
||||
import { TraceReducer } from 'types/reducer/trace';
|
||||
|
||||
import { Container, IconContainer, SelectComponent } from './styles';
|
||||
import TagsKey from './TagKey';
|
||||
import TagValue from './TagValue';
|
||||
import { mapOperators } from './utils';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
type Tags = FlatArray<TraceReducer['selectedTags'], 1>['Operator'];
|
||||
const StringBoolNumber = ['string', 'number', 'bool'];
|
||||
const Number = ['number'];
|
||||
const String = ['string'];
|
||||
export interface AllMenuProps {
|
||||
key: Tags | '';
|
||||
value: string;
|
||||
supportedTypes: string[];
|
||||
}
|
||||
|
||||
export const AllMenu: AllMenuProps[] = [
|
||||
{
|
||||
key: 'Equals',
|
||||
value: 'EQUALS',
|
||||
supportedTypes: StringBoolNumber,
|
||||
},
|
||||
{
|
||||
key: 'NotEquals',
|
||||
value: 'NOT EQUALS',
|
||||
supportedTypes: StringBoolNumber,
|
||||
},
|
||||
{
|
||||
key: 'In',
|
||||
value: 'IN',
|
||||
supportedTypes: String,
|
||||
},
|
||||
{
|
||||
key: 'NotIn',
|
||||
value: 'NOT IN',
|
||||
supportedTypes: String,
|
||||
},
|
||||
{
|
||||
key: 'Exists',
|
||||
value: 'EXISTS',
|
||||
supportedTypes: StringBoolNumber,
|
||||
},
|
||||
{
|
||||
key: 'NotExists',
|
||||
value: 'NOT EXISTS',
|
||||
supportedTypes: StringBoolNumber,
|
||||
},
|
||||
{
|
||||
key: 'GreaterThan',
|
||||
value: 'GREATER THAN',
|
||||
supportedTypes: Number,
|
||||
},
|
||||
{
|
||||
key: 'LessThan',
|
||||
value: 'LESS THAN',
|
||||
supportedTypes: Number,
|
||||
},
|
||||
{
|
||||
key: 'GreaterThanEquals',
|
||||
value: 'GREATER THAN OR EQUALS',
|
||||
supportedTypes: Number,
|
||||
},
|
||||
{
|
||||
key: 'LessThanEquals',
|
||||
value: 'LESS THAN OR EQUALS',
|
||||
supportedTypes: Number,
|
||||
},
|
||||
{
|
||||
key: 'StartsWith',
|
||||
value: 'STARTS WITH',
|
||||
supportedTypes: String,
|
||||
},
|
||||
{
|
||||
key: 'NotStartsWith',
|
||||
value: 'NOT STARTS WITH',
|
||||
supportedTypes: String,
|
||||
},
|
||||
{
|
||||
key: 'Contains',
|
||||
value: 'CONTAINS',
|
||||
supportedTypes: String,
|
||||
},
|
||||
{
|
||||
key: 'NotContains',
|
||||
value: 'NOT CONTAINS',
|
||||
supportedTypes: String,
|
||||
},
|
||||
];
|
||||
|
||||
function SingleTags(props: AllTagsProps): JSX.Element {
|
||||
const { tag, onCloseHandler, setLocalSelectedTags, index, localSelectedTags } =
|
||||
props;
|
||||
const {
|
||||
Key: selectedKey,
|
||||
Operator: selectedOperator,
|
||||
StringValues: selectedStringValues,
|
||||
NumberValues: selectedNumberValues,
|
||||
BoolValues: selectedBoolValues,
|
||||
} = tag;
|
||||
|
||||
const onDeleteTagHandler = (index: number): void => {
|
||||
onCloseHandler(index);
|
||||
};
|
||||
|
||||
const onChangeOperatorHandler = (key: unknown): void => {
|
||||
if (typeof key === 'string') {
|
||||
setLocalSelectedTags([
|
||||
...localSelectedTags.slice(0, index),
|
||||
{
|
||||
Key: selectedKey,
|
||||
StringValues: selectedStringValues,
|
||||
NumberValues: selectedNumberValues,
|
||||
BoolValues: selectedBoolValues,
|
||||
Operator: key as Tags,
|
||||
},
|
||||
...localSelectedTags.slice(index + 1, localSelectedTags.length),
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TagsKey
|
||||
index={index}
|
||||
tag={tag}
|
||||
setLocalSelectedTags={setLocalSelectedTags}
|
||||
/>
|
||||
<SelectComponent
|
||||
onChange={onChangeOperatorHandler}
|
||||
value={AllMenu.find((e) => e.key === selectedOperator)?.value || ''}
|
||||
>
|
||||
{
|
||||
// filter out the operator that does not include supported type of the selected key
|
||||
mapOperators(selectedKey).map((e) => (
|
||||
<Option key={e.value} value={e.key}>
|
||||
{e.value}
|
||||
</Option>
|
||||
))
|
||||
}
|
||||
</SelectComponent>
|
||||
|
||||
{selectedKey ? (
|
||||
<TagValue
|
||||
index={index}
|
||||
tag={tag}
|
||||
setLocalSelectedTags={setLocalSelectedTags}
|
||||
tagKey={selectedKey}
|
||||
/>
|
||||
) : (
|
||||
<SelectComponent />
|
||||
)}
|
||||
|
||||
<IconContainer role="button" onClick={(): void => onDeleteTagHandler(index)}>
|
||||
<X size="md" />
|
||||
</IconContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
interface AllTagsProps {
|
||||
onCloseHandler: (index: number) => void;
|
||||
index: number;
|
||||
tag: FlatArray<TraceReducer['selectedTags'], 1>;
|
||||
setLocalSelectedTags: Dispatch<SetStateAction<TraceReducer['selectedTags']>>;
|
||||
localSelectedTags: TraceReducer['selectedTags'];
|
||||
}
|
||||
|
||||
export interface Value {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default SingleTags;
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Select, Space } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const SpaceComponent = styled(Space)`
|
||||
&&& {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Container = styled(Space)`
|
||||
&&& {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.ant-space-item:not(:last-child, :nth-child(2)) {
|
||||
width: 100%;
|
||||
}
|
||||
.ant-space-item:nth-child(2) {
|
||||
width: 50%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const IconContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
margin-left: 1.125rem;
|
||||
`;
|
||||
|
||||
export const SelectComponent = styled(Select)`
|
||||
&&& {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
@@ -1,232 +0,0 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { AutoCompleteProps } from 'antd';
|
||||
import type { DefaultOptionType } from 'antd/es/select';
|
||||
import { PayloadProps as TagKeyPayload } from 'types/api/trace/getTagFilters';
|
||||
import { PayloadProps as TagValuePayload } from 'types/api/trace/getTagValue';
|
||||
import { OperatorValues, Tags } from 'types/reducer/trace';
|
||||
|
||||
import { AllMenu, AllMenuProps } from '.';
|
||||
|
||||
export type TagValueTypes = string | number | boolean;
|
||||
/**
|
||||
* @description extract tag filters from payload
|
||||
*/
|
||||
export const extractTagFilters = (
|
||||
payload: TagKeyPayload,
|
||||
): DefaultOptionType[] => {
|
||||
const tagFilters: string[] = [];
|
||||
payload.stringTagKeys.forEach((element) => {
|
||||
tagFilters.push(`${element}.(string)`);
|
||||
});
|
||||
payload.numberTagKeys.forEach((element) => {
|
||||
tagFilters.push(`${element}.(number)`);
|
||||
});
|
||||
payload.boolTagKeys.forEach((element) => {
|
||||
tagFilters.push(`${element}.(bool)`);
|
||||
});
|
||||
return tagFilters.map((e) => ({
|
||||
value: e,
|
||||
label: e,
|
||||
}));
|
||||
};
|
||||
|
||||
export const extractTagType = (tagKey: string): string => {
|
||||
if (tagKey?.includes('.(string)')) {
|
||||
return 'string';
|
||||
}
|
||||
if (tagKey?.includes('.(number)')) {
|
||||
return 'number';
|
||||
}
|
||||
if (tagKey?.includes('.(bool)')) {
|
||||
return 'bool';
|
||||
}
|
||||
return 'string';
|
||||
};
|
||||
|
||||
export const extractTagKey = (tagKey: string): string => {
|
||||
const tag = tagKey.split('.(');
|
||||
if (tag && tag.length > 0) {
|
||||
return tag[0];
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export function onTagValueChange(values: unknown): TagValueTypes[] {
|
||||
const stringValues = values as string[];
|
||||
|
||||
if (!Array.isArray(stringValues) || stringValues.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return values as TagValueTypes[];
|
||||
}
|
||||
|
||||
export function separateTagValues(
|
||||
values: TagValueTypes[],
|
||||
selectedKey: string,
|
||||
): { boolValues: boolean[]; numberValues: number[]; stringValues: string[] } {
|
||||
if (selectedKey.includes('.(bool)')) {
|
||||
const boolValues = values.filter(
|
||||
(value) => typeof value === 'boolean',
|
||||
) as boolean[];
|
||||
|
||||
return {
|
||||
boolValues,
|
||||
numberValues: [],
|
||||
stringValues: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedKey.includes('.(number)')) {
|
||||
const numberValues = values
|
||||
.filter((value) => typeof value === 'number' || !Number.isNaN(Number(value)))
|
||||
.map((value) => Number(value)) as number[];
|
||||
return {
|
||||
boolValues: [],
|
||||
numberValues,
|
||||
stringValues: [],
|
||||
};
|
||||
}
|
||||
|
||||
const stringValues = values.filter(
|
||||
(value) =>
|
||||
typeof value === 'string' &&
|
||||
value !== 'true' &&
|
||||
value !== 'false' &&
|
||||
Number.isNaN(Number(value)),
|
||||
) as string[];
|
||||
|
||||
return {
|
||||
boolValues: [],
|
||||
numberValues: [],
|
||||
stringValues,
|
||||
};
|
||||
}
|
||||
|
||||
export function disableTagValue(
|
||||
selectedOperator: OperatorValues,
|
||||
setLocalValue: Dispatch<SetStateAction<TagValueTypes[]>>,
|
||||
selectedKeys: string,
|
||||
setLocalSelectedTags: Dispatch<SetStateAction<Tags[]>>,
|
||||
index: number,
|
||||
): boolean {
|
||||
if (selectedOperator === 'Exists' || selectedOperator === 'NotExists') {
|
||||
setLocalValue([]);
|
||||
setLocalSelectedTags((tags) => [
|
||||
...tags.slice(0, index),
|
||||
{
|
||||
Key: selectedKeys,
|
||||
Operator: selectedOperator,
|
||||
StringValues: [],
|
||||
NumberValues: [],
|
||||
BoolValues: [],
|
||||
},
|
||||
...tags.slice(index + 1, tags.length),
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export function getInitialLocalValue(
|
||||
selectedNumberValues: number[],
|
||||
selectedBoolValues: boolean[],
|
||||
selectedStringValues: string[],
|
||||
): TagValueTypes[] {
|
||||
return [
|
||||
...selectedBoolValues,
|
||||
...selectedNumberValues,
|
||||
...selectedStringValues,
|
||||
];
|
||||
}
|
||||
|
||||
export function getTagValueOptions(
|
||||
payload: TagValuePayload | null | undefined,
|
||||
tagType: string,
|
||||
): Array<{ label: string; value: TagValueTypes }> | undefined {
|
||||
if (tagType === 'string') {
|
||||
return payload?.stringTagValues?.map((e) => ({
|
||||
label: e,
|
||||
value: e,
|
||||
}));
|
||||
}
|
||||
if (tagType === 'number') {
|
||||
return payload?.numberTagValues?.map((e) => ({
|
||||
label: e.toString(),
|
||||
value: e,
|
||||
}));
|
||||
}
|
||||
if (tagType === 'bool') {
|
||||
return payload?.boolTagValues?.map((e) => ({
|
||||
label: e.toString(),
|
||||
value: e,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getTagKeyOptions(
|
||||
payload: TagKeyPayload | null | undefined,
|
||||
): DefaultOptionType[] {
|
||||
if (payload === null) {
|
||||
return [
|
||||
{
|
||||
value: '',
|
||||
label: 'No tags available',
|
||||
},
|
||||
];
|
||||
}
|
||||
if (payload != null) {
|
||||
return extractTagFilters(payload);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function selectOptions(
|
||||
payload: TagValuePayload | null | undefined,
|
||||
tagType: string,
|
||||
): string[] | boolean[] | number[] | undefined {
|
||||
if (tagType === 'string') {
|
||||
return payload?.stringTagValues;
|
||||
}
|
||||
if (tagType === 'number') {
|
||||
return payload?.numberTagValues;
|
||||
}
|
||||
if (tagType === 'bool') {
|
||||
return payload?.boolTagValues;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function mapOperators(selectedKey: string): AllMenuProps[] {
|
||||
return AllMenu.filter((e) =>
|
||||
e?.supportedTypes?.includes(extractTagType(selectedKey)),
|
||||
);
|
||||
}
|
||||
|
||||
export function onTagKeySelect(
|
||||
value: unknown,
|
||||
options: AutoCompleteProps['options'],
|
||||
setSelectedKey: Dispatch<SetStateAction<string>>,
|
||||
setLocalSelectedTags: Dispatch<SetStateAction<Tags[]>>,
|
||||
index: number,
|
||||
tag: Tags,
|
||||
): void {
|
||||
if (
|
||||
typeof value === 'string' &&
|
||||
options &&
|
||||
options.find((option) => option.value === value)
|
||||
) {
|
||||
setSelectedKey(value);
|
||||
setLocalSelectedTags((tags) => [
|
||||
...tags.slice(0, index),
|
||||
{
|
||||
Key: value,
|
||||
Operator: tag.Operator,
|
||||
StringValues: tag.StringValues,
|
||||
NumberValues: tag.NumberValues,
|
||||
BoolValues: tag.BoolValues,
|
||||
},
|
||||
...tags.slice(index + 1, tags.length),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { ChevronRight, Plus } from '@signozhq/icons';
|
||||
import { Button, Space, Flex } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { UpdateTagIsError } from 'store/actions/trace/updateIsTagsError';
|
||||
import { UpdateTagVisibility } from 'store/actions/trace/updateTagPanelVisiblity';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { TraceReducer } from 'types/reducer/trace';
|
||||
|
||||
import { parseTagsToQuery } from '../util';
|
||||
import {
|
||||
ButtonContainer,
|
||||
Container,
|
||||
CurrentTagsContainer,
|
||||
ErrorContainer,
|
||||
} from './styles';
|
||||
import Tags from './Tag';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function AllTags({
|
||||
updateTagIsError,
|
||||
onChangeHandler,
|
||||
updateTagVisibility,
|
||||
updateFilters,
|
||||
}: AllTagsProps): JSX.Element {
|
||||
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
|
||||
|
||||
const [localSelectedTags, setLocalSelectedTags] = useState<
|
||||
TraceReducer['selectedTags']
|
||||
>(traces.selectedTags);
|
||||
|
||||
const onTagAddHandler = (): void => {
|
||||
setLocalSelectedTags((tags) => [
|
||||
...tags,
|
||||
{
|
||||
Key: '',
|
||||
Operator: 'Equals',
|
||||
StringValues: [],
|
||||
NumberValues: [],
|
||||
BoolValues: [],
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const onCloseHandler = (index: number): void => {
|
||||
setLocalSelectedTags([
|
||||
...localSelectedTags.slice(0, index),
|
||||
...localSelectedTags.slice(index + 1, localSelectedTags.length),
|
||||
]);
|
||||
};
|
||||
|
||||
const onRunQueryHandler = (): void => {
|
||||
const parsedQuery = parseTagsToQuery(localSelectedTags);
|
||||
|
||||
if (parsedQuery.isError) {
|
||||
updateTagIsError(true);
|
||||
} else {
|
||||
onChangeHandler(parsedQuery.payload);
|
||||
updateFilters(localSelectedTags);
|
||||
updateTagIsError(false);
|
||||
updateTagVisibility(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onResetHandler = (): void => {
|
||||
setLocalSelectedTags([]);
|
||||
};
|
||||
|
||||
if (traces.isTagModalError) {
|
||||
return (
|
||||
<ErrorContainer>
|
||||
<Text style={{ color: 'var(--warning-background)' }}>
|
||||
Unrecognized query format. Please reset your query by clicking `X` in the
|
||||
search bar above.
|
||||
</Text>
|
||||
|
||||
<Text style={{ color: 'var(--warning-background)' }}>
|
||||
Please click on the search bar to get a drop down to select relevant tags
|
||||
</Text>
|
||||
</ErrorContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Typography>Tags</Typography>
|
||||
|
||||
<CurrentTagsContainer>
|
||||
{localSelectedTags.map((tags, index) => (
|
||||
<Tags
|
||||
key={tags.Key}
|
||||
tag={tags}
|
||||
index={index}
|
||||
onCloseHandler={(): void => onCloseHandler(index)}
|
||||
setLocalSelectedTags={setLocalSelectedTags}
|
||||
localSelectedTags={localSelectedTags}
|
||||
/>
|
||||
))}
|
||||
</CurrentTagsContainer>
|
||||
|
||||
<Space wrap direction="horizontal">
|
||||
<Button type="primary" onClick={onTagAddHandler}>
|
||||
<Flex gap={4} align="center">
|
||||
<Plus size="md" />
|
||||
Add Tags Filter
|
||||
</Flex>
|
||||
</Button>
|
||||
|
||||
<Text truncate={1}>
|
||||
Results will include spans with ALL the specified tags ( Rows are `ANDed` )
|
||||
</Text>
|
||||
</Space>
|
||||
|
||||
<ButtonContainer>
|
||||
<Space align="start">
|
||||
<Button onClick={onResetHandler}>Reset</Button>
|
||||
<Button type="primary" onClick={onRunQueryHandler}>
|
||||
<Flex gap={4} align="center">
|
||||
<ChevronRight size="md" />
|
||||
Run Query
|
||||
</Flex>
|
||||
</Button>
|
||||
</Space>
|
||||
</ButtonContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
updateTagIsError: (value: boolean) => void;
|
||||
updateTagVisibility: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
updateTagIsError: bindActionCreators(UpdateTagIsError, dispatch),
|
||||
updateTagVisibility: bindActionCreators(UpdateTagVisibility, dispatch),
|
||||
});
|
||||
|
||||
interface AllTagsProps extends DispatchProps {
|
||||
updateFilters: (tags: TraceReducer['selectedTags']) => void;
|
||||
onChangeHandler: (search: string) => void;
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(AllTags);
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Card } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled(Card)`
|
||||
min-height: 20vh;
|
||||
width: 100%;
|
||||
min-width: 81.5vw;
|
||||
`;
|
||||
|
||||
export const ErrorContainer = styled(Card)`
|
||||
min-height: 20vh;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const ButtonContainer = styled(Card)`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
padding-top: 11px !important;
|
||||
padding-bottom: 11px !important;
|
||||
padding-right: 38.01px !important;
|
||||
|
||||
margin-top: 1rem !important;
|
||||
|
||||
.ant-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const CurrentTagsContainer = styled.div`
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user