Compare commits

..

3 Commits

Author SHA1 Message Date
Naman Verma
c864c4266e chore: remove comment 2026-09-18 15:25:42 +05:30
Naman Verma
26b4072b0d fix: remove area fill mode none 2026-09-18 15:25:42 +05:30
Naman Verma
5c1f92d5fd feat: add plugin schema for area chart panel 2026-09-18 15:25:42 +05:30
228 changed files with 9495 additions and 4119 deletions

View File

@@ -171,14 +171,6 @@ components:
- kind
- spec
type: object
AlertmanagertypesChannelDefect:
enum:
- none
- missing_type
- multiple_notifiers
- unsupported_notifier
- unrepresentable
type: string
AlertmanagertypesChannelEmailConfig:
properties:
headers:
@@ -392,40 +384,6 @@ 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:
@@ -1075,11 +1033,6 @@ components:
- duration
- repeatType
type: object
AlertmanagertypesRepairChannelParams:
properties:
apply:
type: boolean
type: object
AlertmanagertypesRepeatOn:
enum:
- sunday
@@ -3321,6 +3274,53 @@ components:
repeatVariable:
type: string
type: object
DashboardtypesAreaChartAppearance:
properties:
fillMode:
$ref: '#/components/schemas/DashboardtypesAreaFillMode'
fillOpacity:
$ref: '#/components/schemas/DashboardtypesFillOpacity'
lineInterpolation:
$ref: '#/components/schemas/DashboardtypesLineInterpolation'
lineStyle:
$ref: '#/components/schemas/DashboardtypesLineStyle'
showPoints:
type: boolean
spanGaps:
$ref: '#/components/schemas/DashboardtypesSpanGaps'
type: object
DashboardtypesAreaChartPanelSpec:
properties:
axes:
$ref: '#/components/schemas/DashboardtypesAxes'
chartAppearance:
$ref: '#/components/schemas/DashboardtypesAreaChartAppearance'
formatting:
$ref: '#/components/schemas/DashboardtypesPanelFormatting'
legend:
$ref: '#/components/schemas/DashboardtypesLegend'
thresholds:
items:
$ref: '#/components/schemas/DashboardtypesThresholdWithLabel'
nullable: true
type: array
visualization:
$ref: '#/components/schemas/DashboardtypesAreaChartVisualization'
type: object
DashboardtypesAreaChartVisualization:
properties:
fillSpans:
type: boolean
stack:
$ref: '#/components/schemas/DashboardtypesStackMode'
timePreference:
$ref: '#/components/schemas/DashboardtypesTimePreference'
type: object
DashboardtypesAreaFillMode:
enum:
- solid
- gradient
type: string
DashboardtypesAxes:
properties:
isLogScale:
@@ -3552,6 +3552,11 @@ components:
- gradient
- none
type: string
DashboardtypesFillOpacity:
maximum: 1
minimum: 0
nullable: true
type: number
DashboardtypesGettableDashboardV2:
properties:
createdAt:
@@ -4014,6 +4019,7 @@ components:
DashboardtypesPanelPlugin:
discriminator:
mapping:
signoz/AreaChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec'
signoz/BarChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec'
signoz/HistogramPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec'
signoz/ListPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec'
@@ -4026,6 +4032,7 @@ components:
oneOf:
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec'
@@ -4037,6 +4044,7 @@ components:
enum:
- signoz/TimeSeriesPanel
- signoz/BarChartPanel
- signoz/AreaChartPanel
- signoz/NumberPanel
- signoz/PieChartPanel
- signoz/TablePanel
@@ -4044,6 +4052,18 @@ components:
- signoz/ListPanel
- signoz/TextPanel
type: string
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec:
properties:
kind:
enum:
- signoz/AreaChartPanel
type: string
spec:
$ref: '#/components/schemas/DashboardtypesAreaChartPanelSpec'
required:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec:
properties:
kind:
@@ -4374,6 +4394,12 @@ components:
are connected.
type: boolean
type: object
DashboardtypesStackMode:
enum:
- none
- normal
- percent
type: string
DashboardtypesStorableDashboardData:
additionalProperties: {}
type: object
@@ -9732,8 +9758,6 @@ components:
type: string
name:
type: string
origin:
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
updatedAt:
format: date-time
type: string
@@ -9746,7 +9770,6 @@ components:
- fieldContext
- config
- enabled
- origin
type: object
SpantypesSpanMapperConfig:
properties:
@@ -9775,75 +9798,48 @@ 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:
$ref: '#/components/schemas/SpantypesSpanMapperGroupConditionKey'
type: string
nullable: true
type: array
resource:
items:
$ref: '#/components/schemas/SpantypesSpanMapperGroupConditionKey'
type: string
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:
@@ -9851,7 +9847,6 @@ components:
- context
- operation
- priority
- enabled
type: object
SpantypesSpanMapperTestSpan:
properties:
@@ -20384,85 +20379,6 @@ 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

View File

@@ -4,6 +4,7 @@
"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",
@@ -55,6 +56,7 @@
"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",

View File

@@ -14,6 +14,7 @@
"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",
@@ -78,6 +79,7 @@
"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",

View File

@@ -1588,10 +1588,15 @@ 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 },

View File

@@ -53,6 +53,17 @@ 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(

View File

@@ -47,7 +47,9 @@ import {
SomethingWentWrong,
StatusPage,
SupportPage,
TraceDetailOldRedirect,
TraceDetailV3,
TraceFilter,
TracesExplorer,
TracesFunnelDetails,
TracesFunnels,
@@ -130,6 +132,14 @@ 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,
@@ -214,6 +224,13 @@ 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,

View File

@@ -21,7 +21,6 @@ import type {
AlertmanagertypesPostableChannelDTO,
AlertmanagertypesPostableNotificationChannelDTO,
AlertmanagertypesReceiverDTO,
AlertmanagertypesRepairChannelParamsDTO,
AlertmanagertypesTestableNotificationChannelDTO,
AlertmanagertypesUpdatableNotificationChannelDTO,
CreateChannel201,
@@ -36,9 +35,6 @@ import type {
ListNotificationChannels200,
ListNotificationChannelsParams,
RenderErrorResponseDTO,
RepairNotificationChannel200,
RepairNotificationChannelParams,
RepairNotificationChannelPathParameters,
UpdateChannelByIDPathParameters,
UpdateNotificationChannel200,
UpdateNotificationChannelPathParameters,
@@ -1148,113 +1144,6 @@ 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

View File

@@ -596,13 +596,6 @@ 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',
@@ -624,63 +617,6 @@ 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;
}
@@ -1174,6 +1110,32 @@ 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
@@ -2577,13 +2539,6 @@ export interface AlertmanagertypesReceiverDTO {
wechat_configs?: ConfigWechatConfigDTO[];
}
export interface AlertmanagertypesRepairChannelParamsDTO {
/**
* @type boolean
*/
apply?: boolean;
}
export interface AlertmanagertypesTestableNotificationChannelDTO {
config: AlertmanagertypesChannelConfigDTO;
}
@@ -10817,22 +10772,6 @@ 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
*/
@@ -10840,11 +10779,11 @@ export type SpantypesSpanMapperGroupConditionDTO = {
/**
* @type array,null
*/
attributes: SpantypesSpanMapperGroupConditionKeyDTO[] | null;
attributes: string[] | null;
/**
* @type array,null
*/
resource: SpantypesSpanMapperGroupConditionKeyDTO[] | null;
resource: string[] | null;
} | null;
export interface SpantypesSpanMapperGroupDTO {
@@ -10874,7 +10813,6 @@ export interface SpantypesSpanMapperGroupDTO {
* @type string
*/
orgId: string;
origin: SpantypesSpanMapperOriginDTO;
/**
* @type string
* @format date-time
@@ -10884,10 +10822,6 @@ export interface SpantypesSpanMapperGroupDTO {
* @type string
*/
updatedBy?: string;
/**
* @type integer
*/
version: number;
}
export interface SpantypesGettableSpanMapperGroupsDTO {
@@ -10945,16 +10879,11 @@ export enum SpantypesSpanMapperOperationDTO {
}
export interface SpantypesSpanMapperSourceDTO {
context: SpantypesFieldContextDTO;
/**
* @type boolean
*/
enabled: boolean;
/**
* @type string
*/
key: string;
operation: SpantypesSpanMapperOperationDTO;
origin?: SpantypesSpanMapperOriginDTO;
/**
* @type integer
*/
@@ -10996,7 +10925,6 @@ export interface SpantypesSpanMapperDTO {
* @type string
*/
name: string;
origin: SpantypesSpanMapperOriginDTO;
/**
* @type string
* @format date-time
@@ -13556,25 +13484,6 @@ 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;
/**

View File

@@ -0,0 +1,49 @@
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;

View File

@@ -0,0 +1,62 @@
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;

View File

@@ -0,0 +1,65 @@
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;

View File

@@ -0,0 +1,49 @@
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;

View File

@@ -0,0 +1,31 @@
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;

View File

@@ -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 'components/QuickFilters/hooks/useActiveQueryIndex';
import useActiveQueryIndex from './useActiveQueryIndex';
import useCheckboxDisclosure from './useCheckboxDisclosure';
import useCheckboxFilterActions from './useCheckboxFilterActions';
import useCheckboxFilterState from './useCheckboxFilterState';

View File

@@ -15,21 +15,13 @@ 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, panelType, source, lastUsedQuery]);
}, [isListView, source, lastUsedQuery]);
}
export default useActiveQueryIndex;

View File

@@ -56,57 +56,6 @@ 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) =>

View File

@@ -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 'components/QuickFilters/hooks/useActiveQueryIndex';
import useActiveQueryIndex from '../useActiveQueryIndex';
import useCheckboxDisclosure from '../useCheckboxDisclosure';
import useCheckboxFilterActions from '../useCheckboxFilterActions';
import useCheckboxFilterState from '../useCheckboxFilterState';

View File

@@ -1,81 +0,0 @@
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));
});
});

View File

@@ -1,12 +1,11 @@
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 { useFieldValuesSuggestion } from 'hooks/querySuggestions/useFieldValuesSuggestion';
import { BuilderQueryType } from 'types/api/v5/queryRange';
import { FIELD_API_CACHE_TIME } from 'constants/queryCacheTime';
import { DATA_SOURCE_TO_SIGNAL } from 'types/common/queryBuilder';
interface UseFieldValuesProps {
@@ -43,43 +42,32 @@ export function useFieldValues({
endUnixMilli,
enabled,
}: UseFieldValuesProps): UseFieldValuesReturn {
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 { 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 relatedValues: string[] = useMemo(() => {
const values = data?.data?.values;
if (!values) {
return [];
}
@@ -90,9 +78,10 @@ export function useFieldValues({
value !== null && value !== undefined && value !== '',
) || []
);
}, [values]);
}, [data]);
const allValues: string[] = useMemo(() => {
const values = data?.data?.values;
if (!values) {
return [];
}
@@ -112,7 +101,7 @@ export function useFieldValues({
.map((value) => value.toString()) || [];
return [...stringValues, ...numberValues, ...boolValues];
}, [values]);
}, [data]);
return { relatedValues, allValues, isLoading, isFetching };
}

View File

@@ -1,32 +1,36 @@
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 { getMs } from 'utils/timeUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
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,
} from 'constants/traceFilterKeys';
unionTagFilterItems,
} from 'pages/TracesExplorer/Filter/filterUtils';
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 };
export type FilterType = Record<
AllTraceFilterKeys,
{ values: string[] | string; keys: BaseAutocompleteData }
>;
function Duration({
filter,
@@ -35,7 +39,7 @@ function Duration({
}: {
filter: IQuickFiltersConfig;
onFilterChange?: (query: Query) => void;
source: QuickFiltersSource;
source?: QuickFiltersSource;
}): JSX.Element {
const [selectedFilters, setSelectedFilters] =
useState<
@@ -48,11 +52,26 @@ function Duration({
filter.defaultOpen ? 'durationNano' : '',
]);
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const {
currentQuery,
redirectWithQueryBuilderData,
lastUsedQuery,
panelType,
} = useQueryBuilder();
const compositeQuery = useGetCompositeQueryParam();
const activeQueryIndex = useActiveQueryIndex(source);
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]);
// eslint-disable-next-line sonarjs/cognitive-complexity
const syncSelectedFilters = useMemo((): FilterType => {

View File

@@ -1,101 +0,0 @@
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());
}

View File

@@ -35,7 +35,6 @@ 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';
@@ -114,13 +113,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
const shouldShowDropdownInListView =
isListView && source === QuickFiltersSource.TRACES_EXPLORER;
// 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);
const activeQueryIndex = useMemo(() => {
if (isListView) {
return source === QuickFiltersSource.TRACES_EXPLORER
? lastUsedQuery || 0
: 0;
}
return lastUsedQuery || 0;
}, [isListView, source, lastUsedQuery]);
// clear all the filters for the query which is in sync with filters
const handleReset = (): void => {
@@ -167,10 +167,9 @@ 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 || isAIObservabilityRowView
? showQueryName && currentQuery.builder.queryData?.[0]?.queryName
: lastQueryName;
const displayedQueryName = isListView
? showQueryName && currentQuery.builder.queryData?.[0]?.queryName
: lastQueryName;
const handleQueryChange = (value: number): void => {
setLastUsedQuery(value);
@@ -183,9 +182,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
<Typography.Text className="text">
{displayedQueryName ? 'Filters for' : 'Filters'}
</Typography.Text>
{queryOptions.length > 1 &&
!isAIObservabilityRowView &&
(!isListView || shouldShowDropdownInListView) ? (
{queryOptions.length > 1 && (!isListView || shouldShowDropdownInListView) ? (
<Combobox open={open} onOpenChange={setOpen}>
<ComboboxTrigger
placeholder="Select a query"
@@ -321,7 +318,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
return (
<Duration
key={filter.attributeKey.key}
source={source}
filter={filter}
onFilterChange={onFilterChange}
/>

View File

@@ -1,14 +1,12 @@
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,
@@ -43,31 +41,23 @@ function OtherFilters({
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
}): JSX.Element {
const isMeterDataSource = signal === SignalType.METER_EXPLORER;
const isAIObservability = signal === SignalType.AI_OBSERVABILITY;
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 { 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 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[] = (fetchedKeys ?? []).map((attr) => ({
const suggestions: TelemetryFieldKey[] = rawSuggestions.map((attr) => ({
name: attr.name,
signal: attr.signal as TelemetryFieldKey['signal'],
fieldContext: attr.fieldContext as FieldContext,
@@ -81,7 +71,7 @@ function OtherFilters({
),
);
return suggestions.filter((attr) => !addedKeys.has(attr.key as string));
}, [fetchedKeys, addedFilters]);
}, [data, addedFilters]);
const handleAddFilter = (filter: TelemetryFieldKey): void => {
setAddedFilters((prev) => [...prev, filter]);

View File

@@ -1,81 +0,0 @@
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());
});
});

View File

@@ -7,5 +7,4 @@ 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,
};

View File

@@ -1,81 +0,0 @@
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);
});
});
});

View File

@@ -24,7 +24,6 @@ export enum SignalType {
API_MONITORING = 'api_monitoring',
EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
AI_OBSERVABILITY = 'ai_observability',
}
/**
@@ -70,7 +69,6 @@ export enum QuickFiltersSource {
API_MONITORING = 'api-monitoring',
EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
AI_OBSERVABILITY = 'ai-observability',
}
/**

View File

@@ -41,6 +41,7 @@ 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',

View File

@@ -109,9 +109,6 @@ 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;

View File

@@ -1,4 +1,4 @@
import { OperatorValues } from 'hooks/useResourceAttribute/types';
import { OperatorValues } from 'types/reducer/trace';
export const OperatorConversions: Array<{
label: string;

View File

@@ -6,7 +6,9 @@ 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',

View File

@@ -1,124 +0,0 @@
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;

View File

@@ -0,0 +1,16 @@
.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;
}
}

View File

@@ -0,0 +1,96 @@
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;

View File

@@ -0,0 +1,52 @@
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;
}
`;

View File

@@ -0,0 +1,22 @@
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;

View File

@@ -0,0 +1,41 @@
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;
`;

View File

@@ -0,0 +1,233 @@
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;

View File

@@ -0,0 +1,113 @@
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,
};

View File

@@ -0,0 +1,3 @@
export const getIconStyles = (): Record<string, string> => ({
color: 'var(--l1-foreground)',
});

View File

@@ -0,0 +1,91 @@
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;

View File

@@ -0,0 +1,48 @@
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;
`;

View File

@@ -0,0 +1,203 @@
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;
};

View File

@@ -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 'hooks/useResourceAttribute/types';
import { Tags } from 'types/reducer/trace';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';

View File

@@ -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 'utils/timeUtils';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import {
BlockLink,
getTraceLink,

View File

@@ -333,7 +333,6 @@ describe('AttributeMappingsTab (integration)', () => {
context: FieldContext.attribute,
operation: MapperOperation.copy,
priority,
enabled: true,
})),
},
}),

View File

@@ -1,12 +1,10 @@
import { Typography } from '@signozhq/ui/typography';
import { ConditionKey } from 'container/LLMObservability/AttributeMapping/types';
import styles from './ConditionsTooltip.module.scss';
interface ConditionsTooltipProps {
attributes: ConditionKey[];
resource: ConditionKey[];
attributes: string[];
resource: string[];
}
function ConditionsTooltip({
@@ -35,8 +33,8 @@ function ConditionsTooltip({
</Typography.Text>
<div className={styles.keyList}>
{attributes.map((key) => (
<code key={`${key.origin}-${key.value}`} className={styles.key}>
{key.value}
<code key={key} className={styles.key}>
{key}
</code>
))}
</div>
@@ -49,8 +47,8 @@ function ConditionsTooltip({
</Typography.Text>
<div className={styles.keyList}>
{resource.map((key) => (
<code key={`${key.origin}-${key.value}`} className={styles.key}>
{key.value}
<code key={key} className={styles.key}>
{key}
</code>
))}
</div>

View File

@@ -3,7 +3,6 @@ import {
SpantypesSpanMapperDTO as Mapper,
SpantypesSpanMapperGroupDTO as MapperGroup,
SpantypesSpanMapperOperationDTO as MapperOperation,
SpantypesSpanMapperOriginDTO as MapperOrigin,
SpantypesSpanMapperTestSpanDTO as TestSpan,
} from 'api/generated/services/sigNoz.schemas';
@@ -22,15 +21,9 @@ export function makeGroup(overrides: Partial<MapperGroup> = {}): MapperGroup {
orgId: 'org-1',
name: 'demo',
enabled: true,
origin: MapperOrigin.user,
version: 0,
condition: {
attributes: [
{ value: 'ai.embeddings', enabled: true, origin: MapperOrigin.user },
],
resource: [
{ value: 'cloud.account.id', enabled: true, origin: MapperOrigin.user },
],
attributes: ['ai.embeddings'],
resource: ['cloud.account.id'],
},
...overrides,
};
@@ -42,7 +35,6 @@ 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: [
@@ -51,16 +43,12 @@ 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,
},
],
},
@@ -97,12 +85,8 @@ export const mockGroups: MapperGroup[] = [
id: 'group-1',
name: 'demo',
condition: {
attributes: [
{ value: 'ai.embeddings', enabled: true, origin: MapperOrigin.user },
],
resource: [
{ value: 'cloud.account.id', enabled: true, origin: MapperOrigin.user },
],
attributes: ['ai.embeddings'],
resource: ['cloud.account.id'],
},
}),
makeGroup({

View File

@@ -1,23 +1,19 @@
import { Button } from '@signozhq/ui/button';
import { Plus, X } from '@signozhq/icons';
import {
ConditionKey,
FieldContextValue,
} from 'container/LLMObservability/AttributeMapping/types';
import { createConditionKey } from 'container/LLMObservability/AttributeMapping/utils';
import { FieldContextValue } from 'container/LLMObservability/AttributeMapping/types';
import KeySearchInput from '../../../KeySearchInput/KeySearchInput';
import styles from './ConditionKeyList.module.scss';
interface ConditionKeyListProps {
label: string;
labelHint?: string;
keys: ConditionKey[];
keys: string[];
placeholder: string;
addLabel: string;
testIdPrefix: string;
fieldContext: FieldContextValue;
onChange: (keys: ConditionKey[]) => void;
onChange: (keys: string[]) => void;
}
function ConditionKeyList({
@@ -31,11 +27,11 @@ function ConditionKeyList({
onChange,
}: ConditionKeyListProps): JSX.Element {
const updateKey = (index: number, value: string): void => {
onChange(keys.map((key, i) => (i === index ? { ...key, value } : key)));
onChange(keys.map((key, i) => (i === index ? value : key)));
};
const addKey = (): void => {
onChange([...keys, createConditionKey()]);
onChange([...keys, '']);
};
const removeKey = (index: number): void => {
@@ -57,7 +53,7 @@ function ConditionKeyList({
<KeySearchInput
className={styles.keyInput}
placeholder={placeholder}
value={key.value}
value={key}
fieldContext={fieldContext}
onChange={(next): void => updateKey(index, next)}
testId={`${testIdPrefix}-${index}`}

View File

@@ -42,9 +42,7 @@ 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.enabled === b[index].enabled &&
source.origin === b[index].origin,
source.operation === b[index].operation,
)
);
}

View File

@@ -1,11 +1,8 @@
import {
SpantypesFieldContextDTO,
SpantypesSpanMapperDTO,
SpantypesSpanMapperGroupConditionKeyDTO,
SpantypesSpanMapperGroupDTO,
SpantypesSpanMapperOperationDTO,
SpantypesSpanMapperOriginDTO,
SpantypesSpanMapperSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
export type MapperGroup = SpantypesSpanMapperGroupDTO;
@@ -14,23 +11,21 @@ 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';
// `priority` is left out: it is derived from list order when the draft is
// serialized.
export type SourceConfig = Omit<SpantypesSpanMapperSourceDTO, 'priority'>;
export interface SourceConfig {
key: string;
context: SpantypesFieldContextDTO;
operation: SpantypesSpanMapperOperationDTO;
}
// 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: FieldContextValue;
fieldContext: SpantypesFieldContextDTO;
sources: SourceConfig[];
enabled: boolean;
}
@@ -38,20 +33,26 @@ export interface MapperDraft {
export interface GroupDraft {
id: string | null;
name: string;
attributes: ConditionKey[];
resource: ConditionKey[];
attributes: string[];
resource: string[];
enabled: boolean;
}
// 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'> & {
export interface DraftMapper {
localId: string;
serverId: string | null;
};
name: string;
fieldContext: SpantypesFieldContextDTO;
sources: SourceConfig[];
enabled: boolean;
}
export type DraftGroup = Omit<GroupDraft, 'id'> & {
export interface DraftGroup {
localId: string;
serverId: string | null;
name: string;
attributes: string[];
resource: string[];
enabled: boolean;
mappers: DraftMapper[];
};
}

View File

@@ -1,14 +1,12 @@
import {
SpantypesPostableSpanMapperDTO,
SpantypesPostableSpanMapperGroupDTO,
SpantypesSpanMapperGroupConditionKeyDTO,
SpantypesUpdatableSpanMapperDTO,
SpantypesUpdatableSpanMapperGroupDTO,
} from 'api/generated/services/sigNoz.schemas';
import { v4 as uuid } from 'uuid';
import {
ConditionKey,
DraftGroup,
DraftMapper,
FieldContext,
@@ -17,7 +15,6 @@ import {
MapperDraft,
MapperGroup,
MapperOperation,
MapperOrigin,
SourceConfig,
} from './types';
@@ -27,36 +24,20 @@ function genLocalId(prefix: 'group' | 'mapper'): string {
return `local-${prefix}-${uuid()}`;
}
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[] {
// Trimmed, de-duplicated, non-empty keys preserving input order.
function cleanKeys(keys: string[]): string[] {
const seen = new Set<string>();
const result: ConditionKey[] = [];
const result: string[] = [];
keys.forEach((raw) => {
const value = raw.value.trim();
const dedupeKey = `${raw.origin}:${value}`;
if (value && !seen.has(dedupeKey)) {
seen.add(dedupeKey);
result.push({ ...raw, value });
const key = raw.trim();
if (key && !seen.has(key)) {
seen.add(key);
result.push(key);
}
});
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[] {
@@ -67,8 +48,6 @@ function getMapperSources(mapper: Mapper): SourceConfig[] {
key: source.key,
context: source.context,
operation: source.operation,
enabled: source.enabled,
origin: source.origin ?? MapperOrigin.user,
}));
}
@@ -77,8 +56,6 @@ export function createEmptySource(): SourceConfig {
key: '',
context: FieldContext.attribute,
operation: MapperOperation.copy,
enabled: true,
origin: MapperOrigin.user,
};
}
@@ -95,7 +72,7 @@ function getCleanSources(draft: MapperDraft): SourceConfig[] {
const result: SourceConfig[] = [];
draft.sources.forEach((source) => {
const key = source.key.trim();
const dedupeKey = `${source.origin}:${source.context}:${key}`;
const dedupeKey = `${source.context}:${key}`;
if (key && !seen.has(dedupeKey)) {
seen.add(dedupeKey);
result.push({ ...source, key });
@@ -118,8 +95,6 @@ function buildSources(
context: source.context,
operation: source.operation,
priority: sources.length - index,
enabled: source.enabled,
origin: source.origin,
}));
}
@@ -148,7 +123,7 @@ export function buildUpdatableMapper(
export const EMPTY_GROUP_DRAFT: GroupDraft = {
id: null,
name: '',
attributes: [createConditionKey()],
attributes: [''],
resource: [],
enabled: true,
};
@@ -195,8 +170,8 @@ export function buildDraftGroup(
localId: group.id,
serverId: group.id,
name: group.name,
attributes: fromConditionKeys(group.condition?.attributes),
resource: fromConditionKeys(group.condition?.resource),
attributes: group.condition?.attributes ?? [],
resource: group.condition?.resource ?? [],
enabled: group.enabled,
mappers: mappers.map(buildDraftMapper),
};
@@ -207,8 +182,7 @@ export function groupDraftFromNode(group: DraftGroup): GroupDraft {
return {
id: group.localId,
name: group.name,
attributes:
group.attributes.length > 0 ? group.attributes : [createConditionKey()],
attributes: group.attributes.length > 0 ? group.attributes : [''],
resource: group.resource,
enabled: group.enabled,
};

View File

@@ -8,7 +8,6 @@ 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';
@@ -95,13 +94,6 @@ 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(
@@ -195,9 +187,8 @@ function Explorer(): JSX.Element {
<Card className="filter" hidden={!isOpen}>
<QuickFilters
className="qf-traces-explorer"
source={QuickFiltersSource.AI_OBSERVABILITY}
signal={SignalType.AI_OBSERVABILITY}
useFieldApis={quickFiltersFieldApis}
source={QuickFiltersSource.TRACES_EXPLORER}
signal={SignalType.TRACES}
handleFilterVisibilityChange={(): void => {
setOpen(!isOpen);
}}

View File

@@ -1,7 +1,6 @@
import type { TracesTableRow } from '../TracesTable/getFieldColumn';
import { generatePath } from 'react-router-dom';
import ROUTES from 'constants/routes';
import { formUrlParams } from 'utils/traceUtils';
import { formUrlParams } from 'container/TraceDetail/utils';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
export const getTraceLink = (record: Record<string, unknown>): string => {
@@ -15,11 +14,7 @@ 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);
if (!traceId) {
return '';
}
return `${generatePath(ROUTES.TRACE_DETAIL, { id: traceId })}${formUrlParams({
return `${ROUTES.TRACE}/${traceId}${formUrlParams({
spanId,
levelUp: 0,
levelDown: 0,

View File

@@ -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 'utils/timeUtils';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { useTimezone } from 'providers/Timezone';
import {

View File

@@ -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.TRACES_EXPLORER];
const avialableParams = routeConfig[ROUTES.TRACE];
const queryString = getQueryString(avialableParams, urlParams);
const JSONCompositeQuery = encodeURIComponent(

View File

@@ -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 'hooks/useResourceAttribute/types';
import { Tags } from 'types/reducer/trace';
function TopOperation(): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(

View File

@@ -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 'constants/traceFilterKeys';
import { traceFilterKeys } from 'pages/TracesExplorer/Filter/filterUtils';
import {
BaseAutocompleteData,
DataTypes,
@@ -21,7 +21,7 @@ import {
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { Tags } from 'hooks/useResourceAttribute/types';
import { Tags } from 'types/reducer/trace';
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.TRACES_EXPLORER];
const avialableParams = routeConfig[ROUTES.TRACE];
const queryString = getQueryString(avialableParams, urlParams);
const JSONCompositeQuery = encodeURIComponent(

View File

@@ -56,6 +56,10 @@ jest.mock('../Tabs/util', () => ({
}));
// Mock the resourceAttributesToTracesFilterItems function
jest.mock('container/TraceDetail/utils', () => ({
resourceAttributesToTracesFilterItems: (): any[] => [],
}));
const mockedUseResourceAttribute = useResourceAttribute as jest.MockedFunction<
typeof useResourceAttribute
>;

View File

@@ -0,0 +1,5 @@
.new-explorer-cta-with-badge {
display: inline-flex;
align-items: center;
gap: 6px;
}

View File

@@ -0,0 +1,8 @@
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',
};

View File

@@ -0,0 +1,83 @@
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;

View File

@@ -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 'constants/traceFilterKeys';
import { AllTraceFilterKeyValue } from 'pages/TracesExplorer/Filter/filterUtils';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import {
QueryKeyRequestProps,

View File

@@ -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 'types/styledComponents';
import { StyledCSS } from 'container/GantChart/Trace/styles';
import styled from 'styled-components';
export const FooterButton = styled(Button)`

View File

@@ -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 'hooks/useResourceAttribute/types';
import { Tags } from 'types/reducer/trace';
import SkipOnBoardingModal from '../SkipOnBoardModal';
import ServiceMetricsApplication from './ServiceMetricsApplication';

View File

@@ -15,7 +15,7 @@ import {
import { isUndefined } from 'lodash-es';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'hooks/useResourceAttribute/types';
import { Tags } from 'types/reducer/trace';
import SkipOnBoardingModal from '../SkipOnBoardModal';
import ServiceTraceTable from './ServiceTracesTable';

View File

@@ -42,7 +42,9 @@ 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],

View File

@@ -568,8 +568,7 @@ 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> = {
// Trace detail pages (/trace/:id) highlight the Traces menu item.
'/trace': ROUTES.TRACES_EXPLORER,
[ROUTES.TRACE]: ROUTES.TRACES_EXPLORER,
[ROUTES.TRACE_EXPLORER]: ROUTES.TRACES_EXPLORER,
[ROUTES.LOGS_BASE]: ROUTES.LOGS_EXPLORER,
[ROUTES.METRICS_EXPLORER_BASE]: ROUTES.METRICS_EXPLORER,

View File

@@ -2,6 +2,7 @@ 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';
@@ -11,6 +12,7 @@ interface ToolbarProps {
showAutoRefresh: boolean;
leftActions?: JSX.Element;
rightActions?: JSX.Element;
showOldCTA?: boolean;
warningElement?: JSX.Element;
onGoLive?: () => void;
onExitLiveLogs?: () => void;
@@ -21,6 +23,7 @@ export default function Toolbar({
showAutoRefresh,
leftActions,
rightActions,
showOldCTA,
warningElement,
showLiveLogs,
onGoLive,
@@ -45,6 +48,7 @@ export default function Toolbar({
<div className="rightActions">
<div className="timeRange">
{warningElement}
{showOldCTA && <NewExplorerCTA />}
{showLiveLogs && <LiveLogsPauseResume />}
<DateTimeSelectionV2
showLiveLogs={showLiveLogs}
@@ -65,6 +69,7 @@ export default function Toolbar({
Toolbar.defaultProps = {
leftActions: <div />,
rightActions: <div />,
showOldCTA: false,
warningElement: <div />,
showLiveLogs: false,
onGoLive: (): void => noop(),

View File

@@ -20,6 +20,11 @@ jest.mock('hooks/useSafeNavigate', () => ({
}),
}));
jest.mock('container/NewExplorerCTA', () => ({
__esModule: true,
default: (): null => null,
}));
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
__esModule: true,
default: ({

View File

@@ -20,6 +20,11 @@ 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 =

View File

@@ -61,6 +61,11 @@ 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',

View File

@@ -15,6 +15,11 @@ 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" />,

View File

@@ -20,6 +20,11 @@ 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 =

View File

@@ -143,6 +143,7 @@ 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,

View File

@@ -12,6 +12,7 @@ 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,
@@ -64,6 +65,7 @@ function DateTimeSelection({
updateTimeInterval,
globalTimeLoading,
showResetButton = false,
showOldExplorerCTA = false,
defaultRelativeTime = RelativeTimeMap['6hr'] as Time,
isModalTimeSelection = false,
onTimeChange,
@@ -686,6 +688,12 @@ function DateTimeSelection({
</FormItem>
)}
{showOldExplorerCTA && (
<div style={{ marginRight: 12 }}>
<NewExplorerCTA />
</div>
)}
{!hasSelectedTimeError && !refreshButtonHidden && showRefreshText && (
<div className="refresh-text-container">
<RefreshText
@@ -762,6 +770,7 @@ interface DateTimeSelectionV2Props {
showAutoRefresh: boolean;
showRefreshText?: boolean;
hideShareModal?: boolean;
showOldExplorerCTA?: boolean;
showResetButton?: boolean;
defaultRelativeTime?: Time;
isModalTimeSelection?: boolean;
@@ -783,6 +792,7 @@ interface DateTimeSelectionV2Props {
DateTimeSelection.defaultProps = {
hideShareModal: false,
showOldExplorerCTA: false,
showRefreshText: true,
showResetButton: false,
defaultRelativeTime: RelativeTimeMap['6hr'] as Time,

View File

@@ -3,6 +3,7 @@ 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';
@@ -43,6 +44,7 @@ function TopNav(): JSX.Element | null {
return !isRouteToSkip ? (
<div className="top-nav-container">
<NewExplorerCTA />
<DateTimeSelector showAutoRefresh />
<HeaderRightSection enableShare enableFeedback enableAnnouncements={false} />
</div>

View File

@@ -0,0 +1,200 @@
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;

View File

@@ -0,0 +1,19 @@
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;
}
`;

View File

@@ -0,0 +1,104 @@
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;

View File

@@ -0,0 +1,223 @@
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;

View File

@@ -0,0 +1,43 @@
// 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;
`;

View File

@@ -0,0 +1,13 @@
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);

View File

@@ -0,0 +1,134 @@
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;

View File

@@ -0,0 +1,43 @@
// 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;

View File

@@ -0,0 +1,360 @@
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;

View File

@@ -0,0 +1,50 @@
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;
}
`;

View File

@@ -0,0 +1,30 @@
// 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;

View File

@@ -0,0 +1,29 @@
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;

View File

@@ -0,0 +1,126 @@
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;
};

View File

@@ -0,0 +1,62 @@
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;

View File

@@ -0,0 +1,23 @@
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;
`}
`;

View File

@@ -0,0 +1,97 @@
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;

View File

@@ -0,0 +1,154 @@
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);

View File

@@ -0,0 +1,180 @@
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;

View File

@@ -0,0 +1,38 @@
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%;
}
`;

View File

@@ -0,0 +1,232 @@
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),
]);
}
}

View File

@@ -0,0 +1,153 @@
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);

View File

@@ -0,0 +1,39 @@
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;
`;

View File

@@ -0,0 +1 @@
export const contentStyle = { padding: 0 };

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