Compare commits

...

12 Commits

Author SHA1 Message Date
Aditya Singh
69e6a54569 Merge branch 'main' into refactor/remove-old-trace-explorer 2026-09-20 09:52:45 +05:30
aks07
6a9448ae5f refactor(traces): drop ROUTES.TRACE and build trace-detail links with generatePath
With the old explorer gone, ROUTES.TRACE ('/trace') no longer names a
route. Four call sites were still using it as a string prefix to build
trace-detail links (`${ROUTES.TRACE}/${id}`), which only worked because
/trace/:id shares the prefix. Switch them to
generatePath(ROUTES.TRACE_DETAIL, { id }), the idiom TracesTable/FieldCell
already uses, and remove the constant together with its permission rows,
SideNav param config and Private-route test row.

The two explorer list views previously produced "/trace/?spanId=..." when a
row had no trace ID; they now return an empty link instead of letting
generatePath throw on a missing param.

The APM "view traces" handlers read routeConfig[ROUTES.TRACE] only to pick
which query params to carry over; they now read the TRACES_EXPLORER entry,
which has the same value and is where they navigate.

NEW_ROUTES_MENU_ITEM_KEY_MAP keeps a '/trace' key: the sidebar strips the
URL to its first segment, so this is what highlights the Traces menu item
on /trace/:id detail pages.

No redirect is added for bare /trace: it now falls through to the 404 page,
matching what #12914 did for the old logs explorer.
2026-09-19 15:53:47 +05:30
aks07
a3aa45d31b refactor(explorer): remove NewExplorerCTA
NewExplorerCTA toggled between an explorer and its legacy counterpart:
/logs-explorer <-> /logs/old-logs-explorer and /traces-explorer <-> /trace.
The old logs explorer is removed in #12914 and the old trace explorer
earlier in this series, so the component has no route left to render on.
Remove it, its render sites in TopNav, DateTimeSelectionV2 and Toolbar,
the showOldExplorerCTA / showOldCTA props that no caller ever set, and the
five jest mocks for it in the DateTimeSelectionV2 tests.
2026-09-19 15:52:29 +05:30
aks07
9dcccf24dc refactor(traces): remove the /trace-old redirect
/trace-old/:id was added in #10973 as the target of a "Switch to legacy
trace view" button on the V3 trace page. #11732 removed that button and the
legacy page, leaving the route as a redirect back to /trace/:id. Nothing
else ever emitted /trace-old links (no backend, alert or doc references),
so the only way to have reached it was that button. Remove the redirect
page, route, constant, permission rows, title, SideNav/DateTimeSelection
entries, its Private-route test row and the TRACE_DETAILS_PREFER_OLD_VIEW
localStorage key that has had no reader or writer since #11732.
2026-09-19 15:50:58 +05:30
aks07
af7d4b5c58 refactor(traces): remove the legacy Gantt chart, flame graph and TraceDetail utils
container/GantChart and container/TraceFlameGraph rendered the pre-V3
trace detail page. Neither component has a consumer any more; the only
imports left were a StyledCSS type alias and an ITraceMetaData type, and
StyledCSS was moved out earlier in this series. container/TraceDetail had
shrunk to a utils file whose live helpers were moved to utils/traceUtils;
what remained served only the Gantt chart.

TopOperationsTable.test.tsx mocked container/TraceDetail/utils with a
function that module never exported; the mock is dropped with the module.
2026-09-19 15:50:52 +05:30
aks07
66260542ca refactor(traces): remove the old /trace explorer and its redux slice
pages/Trace was the redux-driven trace explorer that predates
/traces-explorer. Its API clients call /getSpanFilters, /getFilteredSpans,
/getFilteredSpans/aggregates, /getTagFilters and /getTagValues, none of
which exist in the backend since #6464, so every request from the page has
404ed since then.

Remove the page, the container/Trace components, the store/actions/trace
thunks, the `traces` reducer and its action union member, the five dead
api/trace clients with their types, the route registration and page title.
The five api/trace modules still used by TraceDetailsV3 (percentiles,
flamegraph, trace v3/v4, trace item) are kept.

The ROUTES.TRACE constant, its permission rows and its Private-route test
row are removed in a later commit, once the remaining consumers of the
constant have been fixed.
2026-09-19 15:50:48 +05:30
aks07
9d410b3c41 refactor(quick-filters): move DurationSection and split filterUtils
With the old filter panel gone, pages/TracesExplorer/Filter held only two
things with live consumers:

- DurationSection.tsx, used solely by the QuickFilters Duration renderer.
  It moves next to that renderer.
- filterUtils.ts, a grab bag. The trace attribute key catalogue
  (AllTraceFilterKeyValue, AllTraceFilterKeys, traceFilterKeys) is read by
  the options menu, the APM "view traces" links and quick filters, so it
  becomes constants/traceFilterKeys. The filter-state helpers (FilterType,
  addFilter, unionTagFilterItems, HandleRunProps) are only used by the
  Duration renderer and move beside it. The remaining exports had no
  consumers and are dropped, including useGetAggregateValues, which
  duplicated hooks/queryBuilder/useGetAggregateValues.

Duration.tsx's local FilterType alias becomes a re-export of the shared one.
2026-09-19 15:47:44 +05:30
aks07
818e4f4476 refactor(traces): remove the pre-QuickFilters trace filter panel
pages/TracesExplorer/Filter/{Filter,Section,SectionContent}.tsx was the
explorer's left-hand filter panel before components/QuickFilters replaced
it. Nothing in the app renders it any more; only its own describe block in
TracesExplorer.test.tsx did. Remove the components, their stylesheet, that
describe block and the test helpers that existed solely for it. The live
explorer tests in the second describe block are untouched.

DurationSection.tsx and filterUtils.ts in the same folder still have live
consumers and are relocated in the next commit.
2026-09-19 15:47:09 +05:30
aks07
1ccdb198cd refactor(traces): move the live trace-detail helpers to utils/traceUtils
convertTimeToRelevantUnit, formUrlParams and the interval-unit table are
used by TraceDetailsV3, the traces and LLM explorer list views. They lived
in container/TraceDetail/utils, a leftover of the pre-V3 trace detail page
whose other exports only serve the legacy Gantt chart. Move the live ones
to utils/traceUtils and repoint the three test mocks; the old path
re-exports them until the folder is deleted.
2026-09-19 15:44:29 +05:30
aks07
06d4e61d8d refactor(styles): move the StyledCSS type out of GantChart
WidgetCard and the pipelines list view import the StyledCSS interpolation
type from the legacy Gantt chart's styles file. Give it a neutral home in
types/styledComponents so GantChart can be removed. The old path re-exports
it until then.
2026-09-19 15:43:56 +05:30
aks07
b26eb46bdf refactor(traces): move getMs to utils/timeUtils
getMs formats a nanosecond duration as milliseconds and is used by the
traces explorer, LLM explorer, infra monitoring, quick filters and dashboard
list panels. It lived inside the old /trace explorer's Duration filter, so
move it next to the other time helpers before that tree is deleted. The old
path re-exports it until then.
2026-09-19 15:43:52 +05:30
aks07
fe8482e62c refactor(traces): move Tags and OperatorValues out of the trace reducer types
The resource-attribute filter, service pages and metrics/error API types all
import these two types from types/reducer/trace, which otherwise belongs to
the old /trace explorer. Move them to hooks/useResourceAttribute/types, the
domain that actually uses them, so the reducer types file can be deleted with
the explorer. The old path re-exports them until then.
2026-09-19 15:42:28 +05:30
159 changed files with 337 additions and 8643 deletions

View File

@@ -4,7 +4,6 @@
"GET_STARTED": "SigNoz | Get Started",
"SERVICE_METRICS": "SigNoz | Service Metrics",
"SERVICE_MAP": "SigNoz | Service Map",
"TRACE": "SigNoz | Trace",
"HOME": "SigNoz | Home",
"TRACE_DETAIL": "SigNoz | Trace Detail",
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
@@ -56,7 +55,6 @@
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
"MCP_SERVER": "SigNoz | MCP Server",
"AI_ASSISTANT": "SigNoz | AI Assistant",
"TRACE_DETAIL_OLD": "SigNoz | Trace Detail",
"SERVICE_TOP_LEVEL_OPERATIONS": "SigNoz | Service Operations",
"ROLE_DETAILS": "SigNoz | Role Details",
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",

View File

@@ -14,7 +14,6 @@
"GET_STARTED_AZURE_MONITORING": "SigNoz | Get Started | AZURE",
"GET_STARTED": "SigNoz | Get Started with SigNoz Cloud",
"GET_STARTED_WITH_CLOUD": "SigNoz | Get Started with SigNoz Cloud",
"TRACE": "SigNoz | Trace",
"TRACE_DETAIL": "SigNoz | Trace Detail",
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
"SETTINGS": "SigNoz | Settings",
@@ -79,7 +78,6 @@
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
"MCP_SERVER": "SigNoz | MCP Server",
"AI_ASSISTANT": "SigNoz | AI Assistant",
"TRACE_DETAIL_OLD": "SigNoz | Trace Detail",
"SERVICE_TOP_LEVEL_OPERATIONS": "SigNoz | Service Operations",
"ROLE_DETAILS": "SigNoz | Role Details",
"ROLE_CREATE": "SigNoz | Create Role",

View File

@@ -1588,15 +1588,10 @@ describe('PrivateRoute', () => {
deniedRoles: DENIED_ROLES,
},
TRACES_EXPLORER: { path: ROUTES.TRACES_EXPLORER, deniedRoles: DENIED_ROLES },
TRACE: { path: ROUTES.TRACE, deniedRoles: DENIED_ROLES },
TRACE_DETAIL: {
path: ROUTES.TRACE_DETAIL.replace(':id', 'trace-id-1'),
deniedRoles: DENIED_ROLES,
},
TRACE_DETAIL_OLD: {
path: ROUTES.TRACE_DETAIL_OLD.replace(':id', 'trace-id-1'),
deniedRoles: DENIED_ROLES,
},
// LOGS and LOGS_EXPLORER share a path - matchPath resolves it to whichever
// route definition comes last, and both keys are authz-aware either way.
LOGS: { path: ROUTES.LOGS, deniedRoles: DENIED_ROLES },

View File

@@ -53,17 +53,6 @@ export const TracesFunnelDetails = Loadable(
),
);
export const TraceFilter = Loadable(
() => import(/* webpackChunkName: "Trace Filter Page" */ 'pages/Trace'),
);
export const TraceDetailOldRedirect = Loadable(
() =>
import(
/* webpackChunkName: "TraceDetailOldRedirect" */ 'pages/TraceDetailOldRedirect/index'
),
);
export const TraceDetailV3 = Loadable(
() =>
import(

View File

@@ -47,9 +47,7 @@ import {
SomethingWentWrong,
StatusPage,
SupportPage,
TraceDetailOldRedirect,
TraceDetailV3,
TraceFilter,
TracesExplorer,
TracesFunnelDetails,
TracesFunnels,
@@ -132,14 +130,6 @@ const routes: AppRoutes[] = [
exact: true,
key: 'LOGS_SAVE_VIEWS',
},
// Legacy /trace-old/:id redirects to the current /trace/:id view.
{
path: ROUTES.TRACE_DETAIL_OLD,
exact: true,
component: TraceDetailOldRedirect,
isPrivate: true,
key: 'TRACE_DETAIL_OLD',
},
{
path: ROUTES.TRACE_DETAIL,
exact: true,
@@ -224,13 +214,6 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'ALERT_OVERVIEW',
},
{
path: ROUTES.TRACE,
exact: true,
component: TraceFilter,
isPrivate: true,
key: 'TRACE',
},
{
path: ROUTES.TRACES_EXPLORER,
exact: true,

View File

@@ -1,49 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import omitBy from 'lodash-es/omitBy';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getFilters';
const getFilters = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const duration =
omitBy(props.other, (_, key) => !key.startsWith('duration')) || [];
const nonDuration = omitBy(props.other, (_, key) =>
key.startsWith('duration'),
);
const exclude: string[] = [];
props.isFilterExclude.forEach((value, key) => {
if (value) {
exclude.push(key);
}
});
const response = await axios.post<PayloadProps>(`/getSpanFilters`, {
start: props.start,
end: props.end,
getFilters: props.getFilters,
...nonDuration,
maxDuration: String((duration.duration || [])[0] || ''),
minDuration: String((duration.duration || [])[1] || ''),
exclude,
spanKind: props.spanKind,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getFilters;

View File

@@ -1,62 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import omitBy from 'lodash-es/omitBy';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getSpans';
const getSpans = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const updatedSelectedTags = props.selectedTags.map((e) => ({
Key: `${e.Key}.(string)`,
Operator: e.Operator,
StringValues: e.StringValues,
NumberValues: e.NumberValues,
BoolValues: e.BoolValues,
}));
const exclude: string[] = [];
props.isFilterExclude.forEach((value, key) => {
if (value) {
exclude.push(key);
}
});
const other = Object.fromEntries(props.selectedFilter);
const duration = omitBy(other, (_, key) => !key.startsWith('duration')) || [];
const nonDuration = omitBy(other, (_, key) => key.startsWith('duration'));
const response = await axios.post<PayloadProps>(
`/getFilteredSpans/aggregates`,
{
start: String(props.start),
end: String(props.end),
function: props.function,
groupBy: props.groupBy === 'none' ? '' : props.groupBy,
step: props.step,
tags: updatedSelectedTags,
...nonDuration,
maxDuration: String((duration.duration || [])[0] || ''),
minDuration: String((duration.duration || [])[1] || ''),
exclude,
spanKind: props.spanKind,
},
);
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getSpans;

View File

@@ -1,65 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import omitBy from 'lodash-es/omitBy';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getSpanAggregate';
import { TraceFilterEnum } from 'types/reducer/trace';
const getSpanAggregate = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const preProps = {
start: String(props.start),
end: String(props.end),
limit: props.limit,
offset: props.offset,
order: props.order,
orderParam: props.orderParam,
};
const exclude: TraceFilterEnum[] = [];
props.isFilterExclude.forEach((value, key) => {
if (value) {
exclude.push(key);
}
});
const updatedSelectedTags = props.selectedTags.map((e) => ({
Key: `${e.Key}.(string)`,
Operator: e.Operator,
StringValues: e.StringValues,
NumberValues: e.NumberValues,
BoolValues: e.BoolValues,
}));
const other = Object.fromEntries(props.selectedFilter);
const duration = omitBy(other, (_, key) => !key.startsWith('duration')) || [];
const nonDuration = omitBy(other, (_, key) => key.startsWith('duration'));
const response = await axios.post<PayloadProps>(`/getFilteredSpans`, {
...preProps,
tags: updatedSelectedTags,
...nonDuration,
maxDuration: String((duration.duration || [])[0] || ''),
minDuration: String((duration.duration || [])[1] || ''),
exclude,
spanKind: props.spanKind,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getSpanAggregate;

View File

@@ -1,49 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { omitBy } from 'lodash-es';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getTagFilters';
import { TraceFilterEnum } from 'types/reducer/trace';
const getTagFilters = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const duration =
omitBy(props.other, (_, key) => !key.startsWith('duration')) || [];
const exclude: TraceFilterEnum[] = [];
props.isFilterExclude.forEach((value, key) => {
if (value) {
exclude.push(key);
}
});
const nonDuration = omitBy(props.other, (_, key) =>
key.startsWith('duration'),
);
const response = await axios.post<PayloadProps>(`/getTagFilters`, {
start: String(props.start),
end: String(props.end),
...nonDuration,
maxDuration: String((duration.duration || [])[0] || ''),
minDuration: String((duration.duration || [])[1] || ''),
exclude,
spanKind: props.spanKind,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getTagFilters;

View File

@@ -1,31 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getTagValue';
const getTagValue = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post<PayloadProps>(`/getTagValues`, {
start: props.start.toString(),
end: props.end.toString(),
tagKey: {
Key: props.tagKey.Key,
Type: props.tagKey.Type,
},
spanKind: props.spanKind,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getTagValue;

View File

@@ -6,31 +6,27 @@ import {
IQuickFiltersConfig,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { getMs } from 'utils/timeUtils';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
import { DurationSection } from 'pages/TracesExplorer/Filter/DurationSection';
import {
AllTraceFilterKeys,
AllTraceFilterKeyValue,
HandleRunProps,
traceFilterKeys,
unionTagFilterItems,
} from 'pages/TracesExplorer/Filter/filterUtils';
} from 'constants/traceFilterKeys';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
import { clearFilterFromQuery } from '../shared/filterQuery';
import { SectionActionButton } from '../shared/SectionActionButton/SectionActionButton';
import { DurationSection } from './DurationSection';
import { FilterType, HandleRunProps, unionTagFilterItems } from './utils';
import './Duration.styles.scss';
export type FilterType = Record<
AllTraceFilterKeys,
{ values: string[] | string; keys: BaseAutocompleteData }
>;
export type { FilterType };
function Duration({
filter,

View File

@@ -9,10 +9,12 @@ import {
} from 'react';
import { Input } from 'antd';
import { Slider } from '@signozhq/ui/slider';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { getMs } from 'utils/timeUtils';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { addFilter, FilterType, traceFilterKeys } from './filterUtils';
import { traceFilterKeys } from 'constants/traceFilterKeys';
import { addFilter, FilterType } from './utils';
interface DurationProps {
selectedFilters: FilterType | undefined;

View File

@@ -0,0 +1,101 @@
import { Dispatch, SetStateAction } from 'react';
import { AllTraceFilterKeys } from 'constants/traceFilterKeys';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
export type FilterType = Record<
AllTraceFilterKeys,
{ values: string[] | string; keys: BaseAutocompleteData }
>;
export interface HandleRunProps {
resetAll?: boolean;
clearByType?: AllTraceFilterKeys;
}
function convertToStringArr(value: string | string[] | undefined): string[] {
if (value) {
if (typeof value === 'string') {
return [value];
}
return value;
}
return [];
}
export const addFilter = (
filterType: AllTraceFilterKeys,
value: string,
setSelectedFilters: Dispatch<
SetStateAction<
| Record<
AllTraceFilterKeys,
{ values: string[] | string; keys: BaseAutocompleteData }
>
| undefined
>
>,
keys: BaseAutocompleteData,
): void => {
setSelectedFilters((prevFilters) => {
const isDuration = [
'durationNanoMax',
'durationNanoMin',
'durationNano',
].includes(filterType);
// Convert value to string array
const valueArray = convertToStringArr(value);
// If previous filters are undefined, initialize them
if (!prevFilters) {
return {
[filterType]: { values: isDuration ? value : valueArray, keys },
} as unknown as FilterType;
}
// If the filter type doesn't exist, initialize it
if (!prevFilters[filterType]?.values.length) {
return {
...prevFilters,
[filterType]: { values: isDuration ? value : valueArray, keys },
};
}
// If the value already exists, don't add it again
if (convertToStringArr(prevFilters[filterType].values).includes(value)) {
return prevFilters;
}
// Otherwise, add the value to the existing array
return {
...prevFilters,
[filterType]: {
values: isDuration
? value
: [...convertToStringArr(prevFilters[filterType].values), value],
keys,
},
};
});
};
/** Merges two filter lists; later items win on the same key + operator. */
export function unionTagFilterItems(
items1: TagFilterItem[],
items2: TagFilterItem[],
): TagFilterItem[] {
const unionMap = new Map<string, TagFilterItem>();
items1?.forEach((item) => {
const keyOp = `${item?.key?.key}_${item?.op}`;
unionMap.set(keyOp, item);
});
items2?.forEach((item) => {
const keyOp = `${item?.key?.key}_${item?.op}`;
unionMap.set(keyOp, item);
});
return Array.from(unionMap?.values());
}

View File

@@ -41,7 +41,6 @@ export enum LOCALSTORAGE {
DISMISSED_API_KEYS_DEPRECATION_BANNER = 'DISMISSED_API_KEYS_DEPRECATION_BANNER',
TRACE_DETAILS_SPAN_DETAILS_POSITION = 'TRACE_DETAILS_SPAN_DETAILS_POSITION',
LICENSE_KEY_CALLOUT_DISMISSED = 'LICENSE_KEY_CALLOUT_DISMISSED',
TRACE_DETAILS_PREFER_OLD_VIEW = 'TRACE_DETAILS_PREFER_OLD_VIEW',
DASHBOARD_PREFERENCES = 'DASHBOARD_PREFERENCES',
ACTIVE_SIGNOZ_INSTANCE_URL = 'ACTIVE_SIGNOZ_INSTANCE_URL',
DASHBOARDS_LIST_VISIBLE_COLUMNS = 'DASHBOARDS_LIST_VISIBLE_COLUMNS',

View File

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

View File

@@ -6,9 +6,7 @@ const ROUTES = {
SERVICE_METRICS: '/services/:servicename',
SERVICE_TOP_LEVEL_OPERATIONS: '/services/:servicename/top-level-operations',
SERVICE_MAP: '/service-map',
TRACE: '/trace',
TRACE_DETAIL: '/trace/:id',
TRACE_DETAIL_OLD: '/trace-old/:id',
TRACES_EXPLORER: '/traces-explorer',
ONBOARDING: '/onboarding',
GET_STARTED_WITH_CLOUD: '/get-started-with-signoz-cloud',

View File

@@ -0,0 +1,124 @@
import { SPAN_ATTRIBUTES } from 'container/ApiMonitoring/Explorer/Domains/DomainDetails/constants';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
/** Trace attribute key -> label shown in quick filters, the options menu and APM "view traces" links. */
export const AllTraceFilterKeyValue: Record<string, string> = {
durationNanoMin: 'Duration',
durationNano: 'Duration',
duration_nano: 'Duration',
durationNanoMax: 'Duration',
'deployment.environment': 'Environment',
hasError: 'Status',
has_error: 'Status',
serviceName: 'Service Name',
'service.name': 'service.name',
name: 'Operation / Name',
rpcMethod: 'RPC Method',
'rpc.method': 'RPC Method',
responseStatusCode: 'Status Code',
response_status_code: 'Status Code',
httpHost: 'HTTP Host',
http_host: 'HTTP Host',
httpMethod: 'HTTP Method',
http_method: 'HTTP Method',
httpRoute: 'HTTP Route',
'http.route': 'HTTP Route',
httpUrl: 'HTTP URL',
[SPAN_ATTRIBUTES.HTTP_URL]: 'HTTP URL',
traceID: 'Trace ID',
trace_id: 'Trace ID',
} as const;
export type AllTraceFilterKeys = keyof typeof AllTraceFilterKeyValue;
export const traceFilterKeys: Record<AllTraceFilterKeys, BaseAutocompleteData> =
{
durationNano: {
key: 'durationNano',
dataType: DataTypes.Float64,
type: 'tag',
id: 'durationNano--float64--tag--true',
},
hasError: {
key: 'hasError',
dataType: DataTypes.bool,
type: 'tag',
id: 'hasError--bool--tag--true',
},
serviceName: {
key: 'serviceName',
dataType: DataTypes.String,
type: 'tag',
id: 'serviceName--string--tag--true',
},
'deployment.environment': {
key: 'deployment.environment',
dataType: DataTypes.String,
type: 'resource',
id: 'deployment.environment--string--resource--false',
},
name: {
key: 'name',
dataType: DataTypes.String,
type: 'tag',
id: 'name--string--tag--true',
},
rpcMethod: {
key: 'rpcMethod',
dataType: DataTypes.String,
type: 'tag',
id: 'rpcMethod--string--tag--true',
},
responseStatusCode: {
key: 'responseStatusCode',
dataType: DataTypes.String,
type: 'tag',
id: 'responseStatusCode--string--tag--true',
},
httpHost: {
key: 'httpHost',
dataType: DataTypes.String,
type: 'tag',
id: 'httpHost--string--tag--true',
},
httpMethod: {
key: 'httpMethod',
dataType: DataTypes.String,
type: 'tag',
id: 'httpMethod--string--tag--true',
},
httpRoute: {
key: 'httpRoute',
dataType: DataTypes.String,
type: 'tag',
id: 'httpRoute--string--tag--true',
},
httpUrl: {
key: 'httpUrl',
dataType: DataTypes.String,
type: 'tag',
id: 'httpUrl--string--tag--true',
},
traceID: {
key: 'traceID',
dataType: DataTypes.String,
type: 'tag',
id: 'traceID--string--tag--true',
},
durationNanoMin: {
key: 'durationNanoMin',
dataType: DataTypes.Float64,
type: 'tag',
id: 'durationNanoMin--float64--tag--true',
},
durationNanoMax: {
key: 'durationNanoMax',
dataType: DataTypes.Float64,
type: 'tag',
id: 'durationNanoMax--float64--tag--true',
},
} as const;

View File

@@ -1,16 +0,0 @@
.span-container {
.spanDetails {
position: absolute;
height: 50px;
padding: 8px;
min-width: 150px;
background: lightcyan;
color: black;
bottom: 24px;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
}

View File

@@ -1,96 +0,0 @@
import { useEffect } from 'react';
import { Popover } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useTimezone } from 'providers/Timezone';
import { toFixed } from 'utils/toFixed';
import { SpanBorder, SpanLine, SpanText, SpanWrapper } from './styles';
import '../GantChart.styles.scss';
interface SpanLengthProps {
globalStart: number;
startTime: number;
name: string;
width: string;
leftOffset: string;
bgColor: string;
inMsCount: number;
}
function Span(props: SpanLengthProps): JSX.Element {
const { width, leftOffset, bgColor, inMsCount, startTime, name, globalStart } =
props;
const isDarkMode = useIsDarkMode();
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
const { timezone } = useTimezone();
useEffect(() => {
document.documentElement.scrollTop = document.documentElement.clientHeight;
document.documentElement.scrollLeft = document.documentElement.clientWidth;
}, []);
const getContent = (): JSX.Element => {
const timeStamp = dayjs(startTime)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.TIME_UTC_MS);
const startTimeInMs = startTime - globalStart;
return (
<div>
<Typography.Text style={{ marginBottom: '8px' }}>
{' '}
Duration : {inMsCount}
</Typography.Text>
<br />
<Typography.Text style={{ marginBottom: '8px' }}>
Start Time: {startTimeInMs}ms [{timeStamp}]{' '}
</Typography.Text>
</div>
);
};
return (
<SpanWrapper className="span-container">
<SpanLine
className="spanLine"
isDarkMode={isDarkMode}
bgColor={bgColor}
leftOffset={leftOffset}
width={width}
/>
<div>
<Popover
style={{
left: `${leftOffset}%`,
}}
title={name}
content={getContent()}
trigger="hover"
placement="left"
autoAdjustOverflow
>
<SpanBorder
className="spanTrack"
isDarkMode={isDarkMode}
bgColor={bgColor}
leftOffset={leftOffset}
width={width}
/>
</Popover>
</div>
<SpanText isDarkMode={isDarkMode} leftOffset={leftOffset}>{`${toFixed(
time,
2,
)} ${timeUnitName}`}</SpanText>
</SpanWrapper>
);
}
export default Span;

View File

@@ -1,52 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
import styled from 'styled-components';
interface Props {
width: string;
leftOffset: string;
bgColor: string;
isDarkMode: boolean;
}
export const SpanLine = styled.div<Props>`
width: ${({ leftOffset }): string => `${leftOffset}%`};
height: 0px;
border-bottom: 0.1px solid
${({ isDarkMode }): string => (isDarkMode ? '#303030' : '#c0c0c0')};
top: 50%;
position: absolute;
`;
export const SpanBorder = styled.div<Props>`
background: ${({ bgColor }): string => bgColor};
border-radius: 5px;
height: 0.625rem;
width: ${({ width }): string => `${width}%`};
left: ${({ leftOffset }): string => `${leftOffset}%`};
top: 35%;
position: absolute;
`;
export const SpanWrapper = styled.div`
display: flex;
width: 100%;
flex-direction: row;
align-items: center;
position: relative;
z-index: 2;
min-height: 2rem;
`;
interface SpanTextProps extends Pick<Props, 'leftOffset'> {
isDarkMode: boolean;
}
export const SpanText = styled(Typography.Text)<SpanTextProps>`
&&& {
left: ${({ leftOffset }): string => `${leftOffset}%`};
top: 65%;
position: absolute;
width: max-content;
color: ${({ isDarkMode }): string => (isDarkMode ? '#ACACAC' : '#666')};
font-size: 0.75rem;
}
`;

View File

@@ -1,22 +0,0 @@
import { Container, Service, Span, SpanWrapper } from './styles';
function SpanNameComponent({
name,
serviceName,
}: SpanNameComponentProps): JSX.Element {
return (
<Container title={`${name} ${serviceName}`}>
<SpanWrapper>
<Span truncate={1}>{name}</Span>
<Service truncate={1}>{serviceName}</Service>
</SpanWrapper>
</Container>
);
}
interface SpanNameComponentProps {
name: string;
serviceName: string;
}
export default SpanNameComponent;

View File

@@ -1,41 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
import styled from 'styled-components';
export const Span = styled(Typography.Text)`
&&& {
font-size: 0.75rem;
margin: 0;
/* border-bottom: 1px solid grey; */
}
`;
export const Service = styled(Typography.Text)`
&&& {
color: #acacac;
font-size: 0.75rem;
}
`;
export const SpanWrapper = styled.div`
display: flex;
flex-direction: column;
margin-left: 0.625rem;
width: 10rem;
`;
export const SpanConnector = styled.div`
width: 37px;
border: 1px solid #303030;
height: 0;
`;
export const Container = styled.div`
display: flex;
align-items: center;
justify-content: flex-start;
`;
export const SpanName = styled.div`
width: fit-content;
border-bottom: 1px solid black;
`;

View File

@@ -1,233 +0,0 @@
import {
Dispatch,
MouseEventHandler,
SetStateAction,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { ChevronDown, ChevronRight } from '@signozhq/icons';
import { Col } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { StyledCol, StyledRow } from 'components/Styled';
import {
IIntervalUnit,
SPAN_DETAILS_LEFT_COL_WIDTH,
} from 'container/TraceDetail/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ITraceTree } from 'types/api/trace/getTraceItem';
import { ITraceMetaData } from '..';
import Span from '../Span';
import SpanName from '../SpanName';
import { getMetaDataFromSpanTree, getTopLeftFromBody } from '../utils';
import {
CardComponent,
CardContainer,
CaretContainer,
HoverCard,
styles,
Wrapper,
} from './styles';
import { getIconStyles } from './utils';
function Trace(props: TraceProps): JSX.Element {
const {
name,
activeHoverId,
setActiveHoverId,
globalSpread,
globalStart,
serviceName,
startTime,
value,
serviceColour,
id,
setActiveSelectedId,
activeSelectedId,
level,
activeSpanPath,
isExpandAll,
intervalUnit,
children,
isMissing,
} = props;
const isDarkMode = useIsDarkMode();
const [isOpen, setOpen] = useState<boolean>(activeSpanPath[level] === id);
const localTreeExpandInteraction = useRef<boolean | 0>(0); // Boolean is for the state of the expansion whereas the number i.e. 0 is for skipping the user interaction.
useEffect(() => {
if (localTreeExpandInteraction.current !== 0) {
setOpen(localTreeExpandInteraction.current);
localTreeExpandInteraction.current = 0;
} else if (!isOpen) {
setOpen(activeSpanPath[level] === id);
}
}, [activeSpanPath, isOpen, id, level]);
useEffect(() => {
if (isExpandAll) {
setOpen(isExpandAll);
} else {
setOpen(activeSpanPath[level] === id);
}
}, [isExpandAll, activeSpanPath, id, level]);
const isOnlyChild = children.length === 1;
const [top, setTop] = useState<number>(0);
const ref = useRef<HTMLUListElement>(null);
useEffect(() => {
if (activeSelectedId === id) {
ref.current?.scrollIntoView({
block: 'nearest',
behavior: 'auto',
inline: 'nearest',
});
}
}, [activeSelectedId, id]);
const onMouseEnterHandler = (): void => {
setActiveHoverId(id);
if (ref.current) {
const { top } = getTopLeftFromBody(ref.current);
setTop(top);
}
};
const onMouseLeaveHandler = (): void => {
setActiveHoverId('');
};
const onClick = (): void => {
setActiveSelectedId(id);
};
const onClickTreeExpansion: MouseEventHandler<HTMLDivElement> = (
event,
): void => {
event.stopPropagation();
setOpen((state) => {
localTreeExpandInteraction.current = !isOpen;
return !state;
});
};
const { totalSpans } = getMetaDataFromSpanTree(props);
const inMsCount = value;
const nodeLeftOffset = ((startTime - globalStart) * 1e2) / globalSpread;
const width = (value * 1e2) / (globalSpread * 1e6);
const panelWidth = SPAN_DETAILS_LEFT_COL_WIDTH - level * (16 + 1) - 48;
const iconStyles = useMemo(() => getIconStyles(), []);
const icon = useMemo(
() =>
isOpen ? (
<ChevronDown size="md" style={iconStyles} />
) : (
<ChevronRight size="md" style={iconStyles} />
),
[isOpen, iconStyles],
);
return (
<Wrapper
onMouseEnter={onMouseEnterHandler}
onMouseLeave={onMouseLeaveHandler}
isOnlyChild={isOnlyChild}
ref={ref}
isDarkMode={isDarkMode}
>
<HoverCard
top={top}
isHovered={activeHoverId === id}
isSelected={activeSelectedId === id}
isDarkMode={isDarkMode}
/>
<CardContainer isMissing={isMissing} onClick={onClick}>
<StyledCol flex={`${panelWidth}px`} styledclass={[styles.overFlowHidden]}>
<StyledRow styledclass={[styles.flexNoWrap]}>
<Col>
{totalSpans !== 1 && (
<CardComponent
isOnlyChild={isOnlyChild}
isDarkMode={isDarkMode}
onClick={onClickTreeExpansion}
>
<Typography style={{ wordBreak: 'normal' }}>{totalSpans}</Typography>
<CaretContainer>{icon}</CaretContainer>
</CardComponent>
)}
</Col>
<Col>
<SpanName name={name} serviceName={serviceName} />
</Col>
</StyledRow>
</StyledCol>
<Col flex="1">
<Span
globalStart={globalStart}
startTime={startTime}
name={name}
leftOffset={nodeLeftOffset.toString()}
width={width.toString()}
bgColor={serviceColour}
inMsCount={inMsCount / 1e6}
/>
</Col>
</CardContainer>
{isOpen && (
<>
{children.map((child) => (
<Trace
key={child.id}
activeHoverId={activeHoverId}
setActiveHoverId={setActiveHoverId}
{...child}
globalSpread={globalSpread}
globalStart={globalStart}
setActiveSelectedId={setActiveSelectedId}
activeSelectedId={activeSelectedId}
level={level + 1}
activeSpanPath={activeSpanPath}
isExpandAll={isExpandAll}
intervalUnit={intervalUnit}
isMissing={child.isMissing}
/>
))}
</>
)}
</Wrapper>
);
}
Trace.defaultProps = {
isMissing: false,
};
interface ITraceGlobal {
globalSpread: ITraceMetaData['spread'];
globalStart: ITraceMetaData['globalStart'];
}
interface TraceProps extends ITraceTree, ITraceGlobal {
activeHoverId: string;
setActiveHoverId: Dispatch<SetStateAction<string>>;
setActiveSelectedId: Dispatch<SetStateAction<string>>;
activeSelectedId: string;
level: number;
activeSpanPath: string[];
isExpandAll: boolean;
intervalUnit: IIntervalUnit;
isMissing?: boolean;
}
export default Trace;

View File

@@ -1,113 +0,0 @@
import { volcano } from '@ant-design/colors';
import styled, {
css,
DefaultTheme,
ThemedCssFunction,
} from 'styled-components';
interface Props {
isOnlyChild: boolean;
}
export const Wrapper = styled.ul<Props>`
display: flex;
flex-direction: column;
padding-bottom: 0.5rem;
padding-top: 0.5rem;
position: relative;
z-index: 1;
ul {
border-left: ${({ isOnlyChild }): StyledCSS =>
isOnlyChild && 'none'} !important;
${({ isOnlyChild }): StyledCSS =>
isOnlyChild &&
css`
&:before {
border-left: 1px solid #434343;
display: inline-block;
content: '';
height: 54px;
position: absolute;
left: 0;
top: -35px;
}
`}
}
`;
export const CardContainer = styled.li<{ isMissing?: boolean }>`
display: flex;
width: 100%;
cursor: pointer;
border-radius: 0.25rem;
z-index: 2;
${({ isMissing }): string =>
isMissing ? `border: 1px dashed ${volcano[6]} !important;` : ''}
`;
interface Props {
isDarkMode: boolean;
}
export type StyledCSS =
| ReturnType<ThemedCssFunction<DefaultTheme>>
| string
| false
| undefined;
export const CardComponent = styled.div<Props>`
border: 1px solid
${({ isDarkMode }): StyledCSS => (isDarkMode ? '#434343' : '#333')};
box-sizing: border-box;
border-radius: 2px;
display: flex;
justify-content: center;
align-items: center;
padding: 1px 8px;
background: ${({ isDarkMode }): StyledCSS =>
isDarkMode ? '#1d1d1d' : '#ddd'};
height: 22px;
`;
export const CaretContainer = styled.span`
margin-left: 0.304rem;
`;
interface HoverCardProps {
isHovered: boolean;
isSelected: boolean;
top: number;
isDarkMode: boolean;
}
export const HoverCard = styled.div<HoverCardProps>`
display: ${({ isSelected, isHovered }): string =>
isSelected || isHovered ? 'block' : 'none'};
width: 200%;
background-color: ${({ isHovered, isDarkMode }): string => {
if (isHovered) {
return isDarkMode ? '#262626' : '#ddd';
}
return isDarkMode ? '#4f4f4f' : '#bbb';
}};
position: absolute;
top: 0;
left: -100%;
right: 0;
height: 3rem;
opacity: 0.5;
`;
const flexNoWrap = css`
flex-wrap: nowrap;
`;
const overFlowHidden = css`
overflow: hidden;
`;
export const styles = {
flexNoWrap,
overFlowHidden,
};

View File

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

View File

@@ -1,91 +0,0 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { SquareMinus, SquarePlus } from '@signozhq/icons';
import { IIntervalUnit } from 'container/TraceDetail/utils';
import { ITraceTree } from 'types/api/trace/getTraceItem';
import { CardContainer, CardWrapper, CollapseButton } from './styles';
import Trace from './Trace';
import { getSpanPath } from './utils';
function GanttChart(props: GanttChartProps): JSX.Element {
const {
data,
traceMetaData,
activeHoverId,
setActiveHoverId,
activeSelectedId,
setActiveSelectedId,
spanId,
intervalUnit,
} = props;
const { globalStart, spread: globalSpread } = traceMetaData;
const [isExpandAll, setIsExpandAll] = useState<boolean>(false);
const [activeSpanPath, setActiveSpanPath] = useState<string[]>([]);
useEffect(() => {
setActiveSpanPath(getSpanPath(data, spanId));
}, [spanId, data]);
useEffect(() => {
setActiveSpanPath(getSpanPath(data, activeSelectedId));
}, [activeSelectedId, data]);
const handleCollapse = (): void => {
setIsExpandAll((prev) => !prev);
};
return (
<CardContainer>
<CollapseButton
onClick={handleCollapse}
title={isExpandAll ? 'Collapse All' : 'Expand All'}
>
{isExpandAll ? (
<SquareMinus size={16} style={{ color: 'var(--accent-primary)' }} />
) : (
<SquarePlus size={16} style={{ color: 'var(--accent-primary)' }} />
)}
</CollapseButton>
<CardWrapper>
<Trace
activeHoverId={activeHoverId}
activeSpanPath={activeSpanPath}
setActiveHoverId={setActiveHoverId}
key={data.id}
{...{
...data,
globalSpread,
globalStart,
setActiveSelectedId,
activeSelectedId,
}}
level={0}
isExpandAll={isExpandAll}
intervalUnit={intervalUnit}
/>
</CardWrapper>
</CardContainer>
);
}
export interface ITraceMetaData {
globalEnd: number;
globalStart: number;
levels: number;
spread: number;
totalSpans: number;
}
export interface GanttChartProps {
data: ITraceTree;
traceMetaData: ITraceMetaData;
activeSelectedId: string;
activeHoverId: string;
setActiveHoverId: Dispatch<SetStateAction<string>>;
setActiveSelectedId: Dispatch<SetStateAction<string>>;
spanId: string;
intervalUnit: IIntervalUnit;
}
export default GanttChart;

View File

@@ -1,48 +0,0 @@
import styled from 'styled-components';
export const Wrapper = styled.ul`
padding-left: 0;
position: absolute;
width: 100%;
height: 100%;
ul {
list-style: none;
border-left: 1px solid #434343;
padding-left: 1rem;
width: 100%;
margin: 0px;
}
ul li {
position: relative;
&:before {
position: absolute;
left: -1rem;
top: 10px;
content: '';
height: 1px;
width: 1rem;
background-color: #434343;
}
}
`;
export const CardWrapper = styled.div`
display: flex;
width: 100%;
margin-left: 1rem;
margin-top: 1.5rem;
`;
export const CardContainer = styled.li`
display: flex;
width: 100%;
position: relative;
`;
export const CollapseButton = styled.div`
position: absolute;
top: 0;
`;

View File

@@ -1,203 +0,0 @@
import { set } from 'lodash-es';
import { ITraceForest, ITraceTree } from 'types/api/trace/getTraceItem';
interface GetTraceMetaData {
globalStart: number;
globalEnd: number;
spread: number;
totalSpans: number;
levels: number;
}
export const getMetaDataFromSpanTree = (
treeData: ITraceTree,
): GetTraceMetaData => {
let globalStart = Number.POSITIVE_INFINITY;
let globalEnd = Number.NEGATIVE_INFINITY;
let totalSpans = 0;
let levels = 1;
const traverse = (treeNode: ITraceTree, level = 0): void => {
if (!treeNode) {
return;
}
totalSpans += 1;
levels = Math.max(levels, level);
const { startTime } = treeNode;
const endTime = startTime + treeNode.value;
globalStart = Math.min(globalStart, startTime);
globalEnd = Math.max(globalEnd, endTime);
treeNode.children.forEach((childNode) => {
traverse(childNode, level + 1);
});
};
traverse(treeData, 1);
globalStart *= 1e6;
globalEnd *= 1e6;
return {
globalStart,
globalEnd,
spread: globalEnd - globalStart,
totalSpans,
levels,
};
};
export function getTopLeftFromBody(elem: HTMLElement): {
top: number;
left: number;
} {
const box = elem.getBoundingClientRect();
const { body } = document;
const docEl = document.documentElement;
const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
const clientTop = docEl.clientTop || body.clientTop || 0;
const clientLeft = docEl.clientLeft || body.clientLeft || 0;
const top = box.top + scrollTop - clientTop;
const left = box.left + scrollLeft - clientLeft;
return { top: Math.round(top), left: Math.round(left) };
}
export const getNodeById = (
searchingId: string,
treesData: ITraceForest | undefined,
): ITraceForest => {
const newtreeData: ITraceForest = {} as ITraceForest;
const traverse = (
treeNode: ITraceTree,
setCallBack: (arg0: ITraceTree) => void,
level = 0,
): void => {
if (!treeNode) {
return;
}
if (searchingId === treeNode.id) {
setCallBack(treeNode);
}
treeNode.children.forEach((childNode) => {
traverse(childNode, setCallBack, level + 1);
});
};
const spanTreeSetCallback = (
path: (keyof ITraceForest)[],
value: ITraceTree,
): ITraceForest => set(newtreeData, path, [value]);
if (treesData?.spanTree) {
treesData.spanTree.forEach((tree) => {
traverse(tree, (value) => spanTreeSetCallback(['spanTree'], value), 1);
});
}
if (treesData?.missingSpanTree) {
treesData.missingSpanTree.forEach((tree) => {
traverse(
tree,
(value) => spanTreeSetCallback(['missingSpanTree'], value),
1,
);
});
}
return newtreeData;
};
const getSpanWithoutChildren = (
span: ITraceTree,
): Omit<ITraceTree, 'children'> => ({
id: span.id,
name: span.name,
parent: span.parent,
serviceColour: span.serviceColour,
serviceName: span.serviceName,
startTime: span.startTime,
tags: span.tags,
time: span.time,
value: span.value,
event: span.event,
hasError: span.hasError,
spanKind: span.spanKind,
statusCodeString: span.statusCodeString,
statusMessage: span.statusMessage,
});
export const isSpanPresentInSearchString = (
searchedString: string,
tree: ITraceTree,
): boolean => {
const parsedTree = getSpanWithoutChildren(tree);
const stringifyTree = JSON.stringify(parsedTree);
return stringifyTree.includes(searchedString);
};
export const isSpanPresent = (
tree: ITraceTree,
searchedKey: string,
): ITraceTree[] => {
const foundNode: ITraceTree[] = [];
const traverse = (
treeNode: ITraceTree,
level = 0,
foundNode: ITraceTree[],
): void => {
if (!treeNode) {
return;
}
const isPresent = isSpanPresentInSearchString(searchedKey, treeNode);
if (isPresent) {
foundNode.push(treeNode);
}
treeNode.children.forEach((childNode) => {
traverse(childNode, level + 1, foundNode);
});
};
traverse(tree, 1, foundNode);
return foundNode;
};
export const getSpanPath = (tree: ITraceTree, spanId: string): string[] => {
const spanPath: string[] = [];
const traverse = (treeNode: ITraceTree): boolean => {
if (!treeNode) {
return false;
}
spanPath.push(treeNode.id);
if (spanId === treeNode.id) {
return true;
}
let foundInChild = false;
treeNode.children.forEach((childNode) => {
if (traverse(childNode)) {
foundInChild = true;
}
});
if (!foundInChild) {
spanPath.pop();
}
return foundInChild;
};
traverse(tree);
return spanPath;
};

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

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

View File

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

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 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { getMs } from 'utils/timeUtils';
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.TRACE];
const avialableParams = routeConfig[ROUTES.TRACES_EXPLORER];
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 'types/reducer/trace';
import { Tags } from 'hooks/useResourceAttribute/types';
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 'pages/TracesExplorer/Filter/filterUtils';
import { traceFilterKeys } from 'constants/traceFilterKeys';
import {
BaseAutocompleteData,
DataTypes,
@@ -21,7 +21,7 @@ import {
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { Tags } from 'types/reducer/trace';
import { Tags } from 'hooks/useResourceAttribute/types';
import { isModifierKeyPressed } from 'utils/app';
import { secondsToMilliseconds } from 'utils/timeUtils';
import { v4 as uuid } from 'uuid';
@@ -102,7 +102,7 @@ export function onViewTracePopupClick({
urlParams.set(QueryParams.startTime, startTime.toString());
urlParams.set(QueryParams.endTime, endTime.toString());
urlParams.delete(QueryParams.relativeTime);
const avialableParams = routeConfig[ROUTES.TRACE];
const avialableParams = routeConfig[ROUTES.TRACES_EXPLORER];
const queryString = getQueryString(avialableParams, urlParams);
const JSONCompositeQuery = encodeURIComponent(

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
import ROUTES from 'constants/routes';
export const buttonText: Record<string, string> = {
[ROUTES.LOGS_EXPLORER]: 'Old Explorer',
[ROUTES.TRACE]: 'New Explorer',
[ROUTES.OLD_LOGS_EXPLORER]: 'New Explorer',
[ROUTES.TRACES_EXPLORER]: 'Old Explorer',
};

View File

@@ -1,83 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { Button } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { Undo } from '@signozhq/icons';
import { isModifierKeyPressed } from 'utils/app';
import { buttonText } from './config';
import './NewExplorerCTA.styles.scss';
function NewExplorerCTA(): JSX.Element | null {
const location = useLocation();
const { safeNavigate } = useSafeNavigate();
const isTraceOrLogsExplorerPage = useMemo(
() =>
location.pathname === ROUTES.LOGS_EXPLORER ||
location.pathname === ROUTES.TRACE ||
location.pathname === ROUTES.OLD_LOGS_EXPLORER ||
location.pathname === ROUTES.TRACES_EXPLORER,
[location.pathname],
);
const onClickHandler = useCallback(
(e?: React.MouseEvent): void => {
let targetPath: string;
if (location.pathname === ROUTES.LOGS_EXPLORER) {
targetPath = ROUTES.OLD_LOGS_EXPLORER;
} else if (location.pathname === ROUTES.TRACE) {
targetPath = ROUTES.TRACES_EXPLORER;
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
targetPath = ROUTES.LOGS_EXPLORER;
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
targetPath = ROUTES.TRACE;
} else {
return;
}
safeNavigate(targetPath, { newTab: !!e && isModifierKeyPressed(e) });
},
[location.pathname],
);
const button = useMemo(
() => (
<Button
icon={<Undo size={16} />}
onClick={(e): void => onClickHandler(e)}
data-testid="newExplorerCTA"
type="text"
className="periscope-btn link"
>
{buttonText[location.pathname]}
</Button>
),
[location.pathname, onClickHandler],
);
if (!isTraceOrLogsExplorerPage) {
return null;
}
if (location.pathname === ROUTES.TRACES_EXPLORER) {
return button;
}
if (location.pathname === ROUTES.LOGS_EXPLORER) {
return button;
}
return (
<span className="new-explorer-cta-with-badge">
{button}
<Badge color="robin" variant="default">
New
</Badge>
</span>
);
}
export default NewExplorerCTA;

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 'pages/TracesExplorer/Filter/filterUtils';
import { AllTraceFilterKeyValue } from 'constants/traceFilterKeys';
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 'container/GantChart/Trace/styles';
import { StyledCSS } from 'types/styledComponents';
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 'types/reducer/trace';
import { Tags } from 'hooks/useResourceAttribute/types';
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 'types/reducer/trace';
import { Tags } from 'hooks/useResourceAttribute/types';
import SkipOnBoardingModal from '../SkipOnBoardModal';
import ServiceTraceTable from './ServiceTracesTable';

View File

@@ -42,9 +42,7 @@ export const routeConfig: Record<string, QueryParams[]> = {
[ROUTES.SIGN_UP]: [QueryParams.resourceAttributes],
[ROUTES.SOMETHING_WENT_WRONG]: [QueryParams.resourceAttributes],
[ROUTES.TRACES_EXPLORER]: [QueryParams.resourceAttributes],
[ROUTES.TRACE]: [QueryParams.resourceAttributes],
[ROUTES.TRACE_DETAIL]: [QueryParams.resourceAttributes],
[ROUTES.TRACE_DETAIL_OLD]: [QueryParams.resourceAttributes],
[ROUTES.UN_AUTHORIZED]: [QueryParams.resourceAttributes],
[ROUTES.USAGE_EXPLORER]: [QueryParams.resourceAttributes],
[ROUTES.VERSION]: [QueryParams.resourceAttributes],

View File

@@ -568,7 +568,8 @@ export const getUserSettingsDropdownMenuItems = ({
This is used to highlight the correct menu item when the user navigates to a new route
**/
export const NEW_ROUTES_MENU_ITEM_KEY_MAP: Record<string, string> = {
[ROUTES.TRACE]: ROUTES.TRACES_EXPLORER,
// Trace detail pages (/trace/:id) highlight the Traces menu item.
'/trace': ROUTES.TRACES_EXPLORER,
[ROUTES.TRACE_EXPLORER]: ROUTES.TRACES_EXPLORER,
[ROUTES.LOGS_BASE]: ROUTES.LOGS_EXPLORER,
[ROUTES.METRICS_EXPLORER_BASE]: ROUTES.METRICS_EXPLORER,

View File

@@ -2,7 +2,6 @@ import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import ROUTES from 'constants/routes';
import LiveLogsPauseResume from 'container/LiveLogs/LiveLogsPauseResume/LiveLogsPauseResume';
import NewExplorerCTA from 'container/NewExplorerCTA';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { noop } from 'lodash-es';
@@ -12,7 +11,6 @@ interface ToolbarProps {
showAutoRefresh: boolean;
leftActions?: JSX.Element;
rightActions?: JSX.Element;
showOldCTA?: boolean;
warningElement?: JSX.Element;
onGoLive?: () => void;
onExitLiveLogs?: () => void;
@@ -23,7 +21,6 @@ export default function Toolbar({
showAutoRefresh,
leftActions,
rightActions,
showOldCTA,
warningElement,
showLiveLogs,
onGoLive,
@@ -48,7 +45,6 @@ export default function Toolbar({
<div className="rightActions">
<div className="timeRange">
{warningElement}
{showOldCTA && <NewExplorerCTA />}
{showLiveLogs && <LiveLogsPauseResume />}
<DateTimeSelectionV2
showLiveLogs={showLiveLogs}
@@ -69,7 +65,6 @@ export default function Toolbar({
Toolbar.defaultProps = {
leftActions: <div />,
rightActions: <div />,
showOldCTA: false,
warningElement: <div />,
showLiveLogs: false,
onGoLive: (): void => noop(),

View File

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

View File

@@ -20,11 +20,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
}),
}));
jest.mock('container/NewExplorerCTA', () => ({
__esModule: true,
default: (): null => null,
}));
let mockOnCustomDateHandler: ((range: [unknown, unknown]) => void) | null =
null;
let mockOnValidCustomDateChange: ((data: { timeStr: string }) => void) | null =

View File

@@ -61,11 +61,6 @@ jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
),
}));
jest.mock('container/NewExplorerCTA', () => ({
__esModule: true,
default: (): null => null,
}));
function NuqsParamSetter({ paramValue }: { paramValue: string }): JSX.Element {
const [, setYAxisUnit] = useQueryState(
'yAxisUnit',

View File

@@ -15,11 +15,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
}),
}));
jest.mock('container/NewExplorerCTA', () => ({
__esModule: true,
default: (): null => null,
}));
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="custom-time-picker" />,

View File

@@ -20,11 +20,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
}),
}));
jest.mock('container/NewExplorerCTA', () => ({
__esModule: true,
default: (): null => null,
}));
let mockOnCustomDateHandler: ((range: [unknown, unknown]) => void) | null =
null;
let mockOnValidCustomDateChange: ((data: { timeStr: string }) => void) | null =

View File

@@ -143,7 +143,6 @@ export const routesToSkip = [
ROUTES.SETTINGS,
ROUTES.LIST_ALL_ALERT,
ROUTES.TRACE_DETAIL,
ROUTES.TRACE_DETAIL_OLD,
ROUTES.ALL_CHANNELS,
ROUTES.USAGE_EXPLORER,
ROUTES.GET_STARTED_WITH_CLOUD,

View File

@@ -12,7 +12,6 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import NewExplorerCTA from 'container/NewExplorerCTA';
import dayjs, { Dayjs } from 'dayjs';
import {
useGlobalTimeQueryInvalidate,
@@ -65,7 +64,6 @@ function DateTimeSelection({
updateTimeInterval,
globalTimeLoading,
showResetButton = false,
showOldExplorerCTA = false,
defaultRelativeTime = RelativeTimeMap['6hr'] as Time,
isModalTimeSelection = false,
onTimeChange,
@@ -688,12 +686,6 @@ function DateTimeSelection({
</FormItem>
)}
{showOldExplorerCTA && (
<div style={{ marginRight: 12 }}>
<NewExplorerCTA />
</div>
)}
{!hasSelectedTimeError && !refreshButtonHidden && showRefreshText && (
<div className="refresh-text-container">
<RefreshText
@@ -770,7 +762,6 @@ interface DateTimeSelectionV2Props {
showAutoRefresh: boolean;
showRefreshText?: boolean;
hideShareModal?: boolean;
showOldExplorerCTA?: boolean;
showResetButton?: boolean;
defaultRelativeTime?: Time;
isModalTimeSelection?: boolean;
@@ -792,7 +783,6 @@ interface DateTimeSelectionV2Props {
DateTimeSelection.defaultProps = {
hideShareModal: false,
showOldExplorerCTA: false,
showRefreshText: true,
showResetButton: false,
defaultRelativeTime: RelativeTimeMap['6hr'] as Time,

View File

@@ -3,7 +3,6 @@ import { matchPath, useHistory } from 'react-router-dom';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import ROUTES from 'constants/routes';
import NewExplorerCTA from '../NewExplorerCTA';
import DateTimeSelector from './DateTimeSelectionV2';
import { routesToDisable, routesToSkip } from './DateTimeSelectionV2/constants';
@@ -44,7 +43,6 @@ function TopNav(): JSX.Element | null {
return !isRouteToSkip ? (
<div className="top-nav-container">
<NewExplorerCTA />
<DateTimeSelector showAutoRefresh />
<HeaderRightSection enableShare enableFeedback enableAnnouncements={false} />
</div>

View File

@@ -1,200 +0,0 @@
import { useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { Tooltip } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Typography } from '@signozhq/ui/typography';
import getFilters from 'api/trace/getFilters';
import { AxiosError } from 'axios';
import { useNotifications } from 'hooks/useNotifications';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { getFilter, updateURL } from 'store/actions/trace/util';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import { CheckBoxContainer, ParaGraph } from './styles';
function CheckBoxComponent(props: CheckBoxProps): JSX.Element {
const {
selectedFilter,
filterLoading,
filterToFetchData,
spansAggregate,
selectedTags,
filter,
userSelectedFilter,
isFilterExclude,
spanKind,
} = useSelector<AppState, TraceReducer>((state) => state.traces);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { keyValue, name, value } = props;
const dispatch = useDispatch<Dispatch<AppActions>>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const isUserSelected =
(userSelectedFilter.get(name) || []).find((e) => e === keyValue) !==
undefined;
const { notifications } = useNotifications();
// eslint-disable-next-line sonarjs/cognitive-complexity
const onCheckHandler = async (): Promise<void> => {
try {
setIsLoading(true);
const newSelectedMap = new Map(selectedFilter);
const preUserSelectedMap = new Map(userSelectedFilter);
const preIsFilterExclude = new Map(isFilterExclude);
const isTopicPresent = preUserSelectedMap.get(name);
// append the value
if (!isTopicPresent) {
preUserSelectedMap.set(name, [keyValue]);
} else {
const isValuePresent =
isTopicPresent.find((e) => e === keyValue) !== undefined;
// check the value if present then remove the value or isChecked
if (isValuePresent) {
preUserSelectedMap.set(
name,
isTopicPresent.filter((e) => e !== keyValue),
);
} else {
// if not present add into the array of string
preUserSelectedMap.set(name, [...isTopicPresent, keyValue]);
}
}
if (newSelectedMap.get(name)?.find((e) => e === keyValue)) {
newSelectedMap.set(name, [
...(newSelectedMap.get(name) || []).filter((e) => e !== keyValue),
]);
} else {
newSelectedMap.set(name, [
...new Set([...(newSelectedMap.get(name) || []), keyValue]),
]);
}
if (preIsFilterExclude.get(name) !== false) {
preIsFilterExclude.set(name, true);
}
const response = await getFilters({
other: Object.fromEntries(newSelectedMap),
end: String(globalTime.maxTime),
start: String(globalTime.minTime),
getFilters: filterToFetchData.filter((e) => e !== name),
isFilterExclude: preIsFilterExclude,
spanKind,
});
if (response.statusCode === 200) {
const updatedFilter = getFilter(response.payload);
// updatedFilter.forEach((value, key) => {
// if (key !== 'duration' && name !== key) {
// preUserSelectedMap.set(key, Object.keys(value));
// }
// if (key === 'duration') {
// newSelectedMap.set('duration', [value.maxDuration, value.minDuration]);
// }
// });
updatedFilter.set(name, {
[`${keyValue}`]: '-1',
...(filter.get(name) || {}),
...(updatedFilter.get(name) || {}),
});
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
selectedTags,
current: spansAggregate.currentPage,
filter: updatedFilter,
filterToFetchData,
selectedFilter: newSelectedMap,
userSelected: preUserSelectedMap,
isFilterExclude: preIsFilterExclude,
order: spansAggregate.order,
orderParam: spansAggregate.orderParam,
pageSize: spansAggregate.pageSize,
spanKind,
},
});
setIsLoading(false);
updateURL(
newSelectedMap,
filterToFetchData,
spansAggregate.currentPage,
selectedTags,
preIsFilterExclude,
preUserSelectedMap,
spansAggregate.order,
spansAggregate.pageSize,
spansAggregate.orderParam,
);
} else {
setIsLoading(false);
notifications.error({
message: response.error || 'Something went wrong',
});
}
} catch (error) {
notifications.error({
message: (error as AxiosError).toString() || 'Something went wrong',
});
setIsLoading(false);
}
};
const isCheckBoxSelected = isUserSelected;
const TooTipOverLay = useMemo(
(): JSX.Element => <div>{keyValue}</div>,
[keyValue],
);
return (
<CheckBoxContainer>
<Checkbox
disabled={isLoading || filterLoading}
onClick={onCheckHandler}
value={isCheckBoxSelected}
key={keyValue}
>
<Tooltip overlay={TooTipOverLay}>
<ParaGraph truncate={1}>{keyValue}</ParaGraph>
</Tooltip>
</Checkbox>
{isCheckBoxSelected ? (
<Typography>{value}</Typography>
) : (
<Typography>-</Typography>
)}
</CheckBoxContainer>
);
}
interface CheckBoxProps {
keyValue: string;
name: TraceFilterEnum;
value: string;
}
export default CheckBoxComponent;

View File

@@ -1,19 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
import styled from 'styled-components';
export const CheckBoxContainer = styled.div`
display: flex;
justify-content: space-between;
margin-left: 1rem;
margin-right: 1rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
`;
export const ParaGraph = styled(Typography.Text)`
&&& {
margin: 0;
max-width: 8rem;
}
`;

View File

@@ -1,104 +0,0 @@
import { useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { Button, Input } from 'antd';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import { INITIAL_FILTER_VALUE } from 'store/reducers/trace';
import AppActions from 'types/actions';
import { UPDATE_SPAN_UPDATE_FILTER_DISPLAY_VALUE } from 'types/actions/trace';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import CheckBoxComponent from '../Common/Checkbox';
const { Search } = Input;
function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
const { filter, filterDisplayValue } = useSelector<AppState, TraceReducer>(
(state) => state.traces,
);
const { name } = props;
const status = filter.get(name) || {};
const statusObj = Object.keys(status);
const numberOfFilters = filterDisplayValue.get(name) || 0;
const dispatch = useDispatch<Dispatch<AppActions>>();
const [searchFilter, setSearchFilter] = useState<string>('');
const onClickMoreHandler = (): void => {
const newFilterDisplayValue = new Map(filterDisplayValue);
const preValue =
(newFilterDisplayValue.get(name) || 0) + INITIAL_FILTER_VALUE;
newFilterDisplayValue.set(name, preValue);
dispatch({
type: UPDATE_SPAN_UPDATE_FILTER_DISPLAY_VALUE,
payload: newFilterDisplayValue,
});
};
const isMoreButtonAvilable = Boolean(
numberOfFilters && statusObj.length > numberOfFilters,
);
return (
<>
{statusObj.length > 0 && (
<Search
value={searchFilter}
onChange={(e): void => setSearchFilter(e.target.value)}
style={{
padding: '0 3%',
}}
placeholder="Filter Values"
/>
)}
{statusObj
.sort((a, b) => {
const countA = +status[a];
const countB = +status[b];
if (countA === countB) {
return a.length - b.length;
}
return countA - countB;
})
.filter((filter) => {
if (searchFilter.length === 0) {
return true;
}
return filter
.toLocaleLowerCase()
.includes(searchFilter.toLocaleLowerCase());
})
.filter((_, index) => index < numberOfFilters)
.map((e) => (
<CheckBoxComponent
key={e}
{...{
name,
keyValue: e,
value: status[e],
}}
/>
))}
{isMoreButtonAvilable && (
<Button onClick={onClickMoreHandler} type="link">
More
</Button>
)}
</>
);
}
interface CommonCheckBoxProps {
name: TraceFilterEnum;
}
export default CommonCheckBox;

View File

@@ -1,223 +0,0 @@
import {
ChangeEventHandler,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { Slider } from '@signozhq/ui/slider';
import getFilters from 'api/trace/getFilters';
import useDebouncedFn from 'hooks/useDebouncedFunction';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { getFilter, updateURL } from 'store/actions/trace/util';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { Container, InputComponent, InputContainer, Text } from './styles';
import { getMs } from './util';
function Duration(): JSX.Element {
const {
filter,
selectedFilter,
filterToFetchData,
spansAggregate,
selectedTags,
userSelectedFilter,
isFilterExclude,
spanKind,
} = useSelector<AppState, TraceReducer>((state) => state.traces);
const dispatch = useDispatch<Dispatch<AppActions>>();
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const preLocalMaxDuration = useRef<number>();
const preLocalMinDuration = useRef<number>();
const getDuration = useMemo(() => {
const selectedDuration = selectedFilter.get('duration');
if (selectedDuration) {
return {
maxDuration: selectedDuration[0],
minDuration: selectedDuration[1],
};
}
return filter.get('duration') || {};
}, [selectedFilter, filter]);
const [preMax, setPreMax] = useState<string>('');
const [preMin, setPreMin] = useState<string>('');
useEffect(() => {
const duration = getDuration || {};
const maxDuration = duration.maxDuration || '0';
const minDuration = duration.minDuration || '0';
if (preLocalMaxDuration.current === undefined) {
preLocalMaxDuration.current = parseFloat(maxDuration);
}
if (preLocalMinDuration.current === undefined) {
preLocalMinDuration.current = parseFloat(minDuration);
}
setPreMax(getMs(maxDuration));
setPreMin(getMs(minDuration));
}, [getDuration]);
const updatedUrl = async (min: number, max: number): Promise<void> => {
const preSelectedFilter = new Map(selectedFilter);
const preUserSelected = new Map(userSelectedFilter);
preSelectedFilter.set('duration', [
String(max * 1000000),
String(min * 1000000),
]);
const response = await getFilters({
end: String(globalTime.maxTime),
getFilters: filterToFetchData,
other: Object.fromEntries(preSelectedFilter),
start: String(globalTime.minTime),
isFilterExclude,
spanKind,
});
if (response.statusCode === 200) {
const preFilter = getFilter(response.payload);
preFilter.forEach((value, key) => {
const values = Object.keys(value);
if (key !== 'duration' && values.length) {
preUserSelected.set(key, values);
}
});
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
current: spansAggregate.currentPage,
filter: preFilter,
filterToFetchData,
selectedFilter: preSelectedFilter,
selectedTags,
userSelected: preUserSelected,
isFilterExclude,
order: spansAggregate.order,
pageSize: spansAggregate.pageSize,
orderParam: spansAggregate.orderParam,
spanKind,
},
});
updateURL(
preSelectedFilter,
filterToFetchData,
spansAggregate.currentPage,
selectedTags,
isFilterExclude,
userSelectedFilter,
spansAggregate.order,
spansAggregate.pageSize,
spansAggregate.orderParam,
);
}
};
const onRangeSliderHandler = (number: [string, string]): void => {
const [min, max] = number;
setPreMin(min);
setPreMax(max);
};
const debouncedFunction = useDebouncedFn(
(min, max) => {
updatedUrl(min as number, max as number);
},
1500,
undefined,
);
const onChangeMaxHandler: ChangeEventHandler<HTMLInputElement> = (event) => {
const { value } = event.target;
const min = preMin;
const max = value;
onRangeSliderHandler([min, max]);
debouncedFunction(min, max);
};
const onChangeMinHandler: ChangeEventHandler<HTMLInputElement> = (event) => {
const { value } = event.target;
const min = value;
const max = preMax;
onRangeSliderHandler([min, max]);
debouncedFunction(min, max);
};
const onRangeHandler = (value: number | number[]): void => {
const [min, max] = value as number[];
updatedUrl(min, max);
};
const TipComponent = useCallback(
(value: number) => <div>{`${value.toString()}ms`}</div>,
[],
);
return (
<div>
<Container>
<InputContainer>
<Text>Min</Text>
</InputContainer>
<InputComponent
addonAfter="ms"
type="number"
onChange={onChangeMinHandler}
value={preMin}
/>
<InputContainer>
<Text>Max</Text>
</InputContainer>
<InputComponent
addonAfter="ms"
type="number"
onChange={onChangeMaxHandler}
value={preMax}
/>
</Container>
<Container>
<Slider
min={Number(getMs(String(preLocalMinDuration.current || 0)))}
max={Number(getMs(String(preLocalMaxDuration.current || 0)))}
range
tooltip={{ formatter: TipComponent }}
onChange={(value): void => {
const [min, max] = value as number[];
onRangeSliderHandler([String(min), String(max)]);
}}
onAfterChange={onRangeHandler}
value={[Number(preMin), Number(preMax)]}
/>
</Container>
</div>
);
}
export default Duration;

View File

@@ -1,43 +0,0 @@
// TODO(@signozhq/ui-input): migrate this styled(Input) once @signozhq/ui
// Input supports `addonAfter` (the consumer renders `<InputComponent addonAfter="ms">`).
import { Typography } from '@signozhq/ui/typography';
import { Input } from 'antd';
import styled from 'styled-components';
export const DurationText = styled.div`
display: flex;
align-items: center;
justify-content: space-around;
min-height: 8vh;
flex-direction: column;
`;
export const InputComponent = styled(Input)`
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type='number'] {
-moz-appearance: textfield;
}
`;
export const InputContainer = styled.div`
width: 100%;
margin-top: 0.5rem;
margin-bottom: 0.2rem;
`;
export const Text = styled(Typography)`
&&& {
text-align: left;
}
`;
export const Container = styled.div`
padding-left: 1rem;
padding-right: 1rem;
`;

View File

@@ -1,13 +0,0 @@
import dayjs from 'dayjs';
import durationPlugin from 'dayjs/plugin/duration';
dayjs.extend(durationPlugin);
export const getMs = (value: string): string =>
parseFloat(
dayjs
.duration({
milliseconds: parseInt(value, 10) / 1000000,
})
.format('SSS'),
).toFixed(2);

View File

@@ -1,134 +0,0 @@
import { ChangeEvent, useEffect, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { Input } from 'antd';
import getFilters from 'api/trace/getFilters';
import { AxiosError } from 'axios';
import { useNotifications } from 'hooks/useNotifications';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { getFilter, updateURL } from 'store/actions/trace/util';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
const { Search } = Input;
function TraceID(): JSX.Element {
const {
selectedFilter,
filterToFetchData,
spansAggregate,
selectedTags,
userSelectedFilter,
isFilterExclude,
spanKind,
} = useSelector<AppState, TraceReducer>((state) => state.traces);
const dispatch = useDispatch<Dispatch<AppActions>>();
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [isLoading, setIsLoading] = useState(false);
const [userEnteredValue, setUserEnteredValue] = useState<string>('');
const { notifications } = useNotifications();
useEffect(() => {
setUserEnteredValue(selectedFilter.get('traceID')?.[0] || '');
}, [selectedFilter]);
const onSearch = async (value: string): Promise<void> => {
try {
setIsLoading(true);
const preSelectedFilter = new Map(selectedFilter);
const preUserSelected = new Map(userSelectedFilter);
if (value !== '') {
preUserSelected.set('traceID', [value]);
preSelectedFilter.set('traceID', [value]);
} else {
preUserSelected.delete('traceID');
preSelectedFilter.delete('traceID');
}
const response = await getFilters({
other: Object.fromEntries(preSelectedFilter),
end: String(globalTime.maxTime),
start: String(globalTime.minTime),
getFilters: filterToFetchData,
isFilterExclude,
spanKind,
});
if (response.statusCode === 200) {
const preFilter = getFilter(response.payload);
preFilter.set('traceID', { traceID: value });
preFilter.forEach((value, key) => {
const values = Object.keys(value);
if (key !== 'duration' && values.length) {
preUserSelected.set(key, values);
}
});
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
current: spansAggregate.currentPage,
filter: preFilter,
filterToFetchData,
selectedFilter: preSelectedFilter,
selectedTags,
userSelected: preUserSelected,
isFilterExclude,
order: spansAggregate.order,
pageSize: spansAggregate.pageSize,
orderParam: spansAggregate.orderParam,
spanKind,
},
});
updateURL(
preSelectedFilter,
filterToFetchData,
spansAggregate.currentPage,
selectedTags,
isFilterExclude,
userSelectedFilter,
spansAggregate.order,
spansAggregate.pageSize,
spansAggregate.orderParam,
);
}
} catch (error) {
notifications.error({
message: (error as AxiosError).toString() || 'Something went wrong',
});
} finally {
setIsLoading(false);
}
};
const onChange = (e: ChangeEvent<HTMLInputElement>): void => {
setUserEnteredValue(e.target.value);
};
const onBlur = (): void => {
if (userEnteredValue !== selectedFilter.get('traceID')?.[0]) {
onSearch(userEnteredValue);
}
};
return (
<div>
<Search
placeholder="Filter by Trace ID"
onSearch={onSearch}
style={{
marginBottom: '5rem',
padding: '0 3%',
}}
loading={isLoading}
value={userEnteredValue}
onChange={onChange}
onBlur={onBlur}
/>
</div>
);
}
export default TraceID;

View File

@@ -1,43 +0,0 @@
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Card } from 'antd';
import Spinner from 'components/Spinner';
import { AppState } from 'store/reducers';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import CommonCheckBox from './CommonCheckBox';
import Duration from './Duration';
import TraceID from './SearchTraceID';
function PanelBody(props: PanelBodyProps): JSX.Element {
const { type } = props;
const { filterLoading } = useSelector<AppState, TraceReducer>(
(state) => state.traces,
);
if (filterLoading) {
return (
<Card bordered={false}>
<Spinner height="10vh" tip="Loading.." />
</Card>
);
}
const renderBody = (type: TraceFilterEnum): JSX.Element => {
switch (type) {
case 'traceID':
return <TraceID />;
case 'duration':
return <Duration />;
default:
return <CommonCheckBox name={type} />;
}
};
return <Card bordered={false}>{renderBody(type)}</Card>;
}
interface PanelBodyProps {
type: TraceFilterEnum;
}
export default PanelBody;

View File

@@ -1,360 +0,0 @@
import { MouseEventHandler, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { ChevronDown, ChevronRight } from '@signozhq/icons';
import { Card } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import getFilters from 'api/trace/getFilters';
import { AxiosError } from 'axios';
import { useNotifications } from 'hooks/useNotifications';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { getFilter, updateURL } from 'store/actions/trace/util';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
AllPanelHeading,
TraceFilterEnum,
TraceReducer,
} from 'types/reducer/trace';
import {
ButtonComponent,
ButtonContainer,
Container,
IconContainer,
TextContainer,
} from './styles';
const { Text } = Typography;
function PanelHeading(props: PanelHeadingProps): JSX.Element {
const {
filterLoading,
filterToFetchData,
selectedFilter,
spansAggregate,
selectedTags,
filter,
isFilterExclude,
userSelectedFilter,
spanKind,
} = useSelector<AppState, TraceReducer>((state) => state.traces);
const { name: PanelName, isOpen: IsPanelOpen } = props;
const isDefaultOpen =
filterToFetchData.find((e) => e === PanelName) !== undefined;
const [isLoading, setIsLoading] = useState<boolean>(false);
const global = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch<Dispatch<AppActions>>();
const defaultErrorMessage = 'Something went wrong';
const { notifications } = useNotifications();
const onExpandHandler: MouseEventHandler<HTMLDivElement> = async (e) => {
try {
e.preventDefault();
e.stopPropagation();
setIsLoading(true);
let updatedFilterData: TraceReducer['filterToFetchData'] = [];
const getprepdatedSelectedFilter = new Map(selectedFilter);
const getPreUserSelected = new Map(userSelectedFilter);
updatedFilterData = [PanelName];
const response = await getFilters({
end: String(global.maxTime),
start: String(global.minTime),
getFilters: updatedFilterData,
other: Object.fromEntries(getprepdatedSelectedFilter),
isFilterExclude,
spanKind,
});
if (response.statusCode === 200) {
const updatedFilter = getFilter(response.payload);
if (!getPreUserSelected.has(PanelName)) {
getPreUserSelected.set(
PanelName,
Object.keys(updatedFilter.get(PanelName) || []),
);
}
updatedFilterData = [...filterToFetchData, PanelName];
filter.forEach((value, key) => {
if (key !== PanelName) {
updatedFilter.set(key, value);
}
});
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
current: spansAggregate.currentPage,
filter: updatedFilter,
filterToFetchData: updatedFilterData,
selectedFilter: getprepdatedSelectedFilter,
selectedTags,
userSelected: getPreUserSelected,
isFilterExclude,
order: spansAggregate.order,
pageSize: spansAggregate.pageSize,
orderParam: spansAggregate.orderParam,
spanKind,
},
});
updateURL(
getprepdatedSelectedFilter,
updatedFilterData,
spansAggregate.currentPage,
selectedTags,
isFilterExclude,
getPreUserSelected,
spansAggregate.order,
spansAggregate.pageSize,
spansAggregate.orderParam,
);
} else {
notifications.error({
message: response.error || defaultErrorMessage,
});
}
setIsLoading(false);
} catch (error) {
notifications.error({
message: (error as AxiosError).toString() || defaultErrorMessage,
});
}
};
/**
* @description this function removes the selected filter
*/
const onCloseHandler = (): void => {
const preSelectedFilter = new Map(selectedFilter);
// removing the filter from filter to fetch the data
const preFilterToFetchTheData = [
...filterToFetchData.filter((name) => name !== PanelName),
];
// preSelectedFilter.delete(PanelName);
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
current: spansAggregate.currentPage,
filter,
filterToFetchData: preFilterToFetchTheData,
selectedFilter: preSelectedFilter,
selectedTags,
userSelected: userSelectedFilter,
isFilterExclude,
order: spansAggregate.order,
pageSize: spansAggregate.pageSize,
orderParam: spansAggregate.orderParam,
spanKind,
},
});
updateURL(
preSelectedFilter,
preFilterToFetchTheData,
spansAggregate.currentPage,
selectedTags,
isFilterExclude,
userSelectedFilter,
spansAggregate.order,
spansAggregate.pageSize,
spansAggregate.orderParam,
);
};
const onClearAllHandler = async (): Promise<void> => {
try {
setIsLoading(true);
const updatedFilter = new Map(selectedFilter);
const preUserSelected = new Map(userSelectedFilter);
updatedFilter.delete(PanelName);
preUserSelected.delete(PanelName);
const postIsFilterExclude = new Map(isFilterExclude);
postIsFilterExclude.set(PanelName, false);
const response = await getFilters({
end: String(global.maxTime),
start: String(global.minTime),
getFilters: filterToFetchData,
other: Object.fromEntries(updatedFilter),
isFilterExclude: postIsFilterExclude,
spanKind,
});
if (response.statusCode === 200 && response.payload) {
const getUpdatedFilter = getFilter(response.payload);
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
current: spansAggregate.currentPage,
filter: getUpdatedFilter,
filterToFetchData,
selectedFilter: updatedFilter,
selectedTags,
userSelected: preUserSelected,
isFilterExclude: postIsFilterExclude,
order: spansAggregate.order,
pageSize: spansAggregate.pageSize,
orderParam: spansAggregate.orderParam,
spanKind,
},
});
updateURL(
updatedFilter,
filterToFetchData,
spansAggregate.currentPage,
selectedTags,
postIsFilterExclude,
preUserSelected,
spansAggregate.order,
spansAggregate.pageSize,
spansAggregate.orderParam,
);
} else {
notifications.error({
message: response.error || 'Something went wrong',
});
}
setIsLoading(false);
} catch (error) {
notifications.error({
message: (error as AxiosError).toString(),
});
setIsLoading(false);
}
};
// const onSelectAllHandler = async () => {
// try {
// setIsLoading(true);
// const preFilter = new Map(filter);
// const preSelectedFilter = new Map(selectedFilter);
// preSelectedFilter.set(
// props.name,
// Object.keys(preFilter.get(props.name) || {}),
// );
// const response = await getFilters({
// end: String(global.maxTime),
// start: String(global.minTime),
// getFilters: filterToFetchData,
// other: Object.fromEntries(preSelectedFilter),
// });
// if (response.statusCode === 200 && response.payload) {
// const getUpatedFilter = getFilter(response.payload);
// preSelectedFilter.set(
// props.name,
// Object.keys(getUpatedFilter.get(props.name) || {}),
// );
// dispatch({
// type: UPDATE_ALL_FILTERS,
// payload: {
// current: spansAggregate.currentPage,
// filter: preFilter,
// filterToFetchData,
// selectedFilter: preSelectedFilter,
// selectedTags,
// },
// });
// updateURL(
// preSelectedFilter,
// filterToFetchData,
// spansAggregate.currentPage,
// selectedTags,
// preFilter,
// );
// }
// setIsLoading(false);
// } catch (error) {
// setIsLoading(false);
// notification.error({
// message: (error as AxiosError).toString(),
// });
// }
// };
return (
<>
{PanelName !== 'duration' && <Divider plain style={{ margin: 0 }} />}
<Card bordered={false}>
<Container
disabled={filterLoading || isLoading}
aria-disabled={filterLoading || isLoading}
aria-expanded={IsPanelOpen}
>
<TextContainer onClick={isDefaultOpen ? onCloseHandler : onExpandHandler}>
<IconContainer>
{!IsPanelOpen ? <ChevronRight size="lg" /> : <ChevronDown size="lg" />}
</IconContainer>
<Text style={{ textTransform: 'capitalize' }} truncate={1}>
{AllPanelHeading.find((e) => e.key === PanelName)?.displayValue || ''}
</Text>
</TextContainer>
{PanelName !== 'duration' && (
<ButtonContainer>
{/* <ButtonComponent
aria-disabled={isLoading || filterLoading}
disabled={isLoading || filterLoading}
onClick={onSelectAllHandler}
type="link"
>
Select All
</ButtonComponent> */}
<ButtonComponent
aria-disabled={isLoading || filterLoading}
disabled={isLoading || filterLoading}
onClick={onClearAllHandler}
type="link"
>
Clear All
</ButtonComponent>
</ButtonContainer>
)}
</Container>
</Card>
</>
);
}
interface PanelHeadingProps {
name: TraceFilterEnum;
isOpen: boolean;
}
export default PanelHeading;

View File

@@ -1,50 +0,0 @@
import { Button } from 'antd';
import { StyledCSS } from 'container/GantChart/Trace/styles';
import styled, { css } from 'styled-components';
interface Props {
disabled: boolean;
}
export const Container = styled.div<Props>`
&&& {
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 0.5rem;
min-height: 5vh;
cursor: ${({ disabled }): string => (disabled ? 'not-allowed' : '')};
${({ disabled }): StyledCSS =>
disabled &&
css`
opacity: 0.5;
`}
}
`;
export const IconContainer = styled.div`
&&& {
margin-right: 0.5rem;
}
`;
export const TextContainer = styled.div`
&&& {
display: flex;
cursor: pointer;
}
`;
export const ButtonComponent = styled(Button)`
&&& {
font-size: 0.75rem;
}
`;
export const ButtonContainer = styled.div`
&&& {
display: flex;
}
`;

View File

@@ -1,30 +0,0 @@
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import PanelBody from './PanelBody';
import PanelHeading from './PanelHeading';
function Panel(props: PanelProps): JSX.Element {
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const { name } = props;
const isDefaultOpen =
traces.filterToFetchData.find((e) => e === name) !== undefined;
return (
<>
<PanelHeading name={name} isOpen={isDefaultOpen} />
{isDefaultOpen && <PanelBody type={name} />}
</>
);
}
interface PanelProps {
name: TraceFilterEnum;
}
export default Panel;

View File

@@ -1,29 +0,0 @@
import { TraceFilterEnum } from 'types/reducer/trace';
import Panel from './Panel';
export const AllTraceFilterEnum: TraceFilterEnum[] = [
'duration',
'status',
'serviceName',
'operation',
'rpcMethod',
'responseStatusCode',
'httpHost',
'httpMethod',
'httpRoute',
'httpUrl',
'traceID',
];
function Filters(): JSX.Element {
return (
<>
{AllTraceFilterEnum.map((panelName) => (
<Panel key={panelName} name={panelName} />
))}
</>
);
}
export default Filters;

View File

@@ -1,126 +0,0 @@
import { ChartData, ChartDatasetProperties } from 'chart.js';
import dayjs from 'dayjs';
import { colors } from 'lib/getRandomColor';
import { keys } from 'lodash-es';
import { TraceReducer } from 'types/reducer/trace';
function transposeArray(array: number[][], arrayLength: number): number[][] {
const newArray: number[][] = [];
for (let i = 0; i < array.length; i += 1) {
newArray.push([]);
}
for (let i = 0; i < array.length; i += 1) {
for (let j = 0; j < arrayLength; j += 1) {
newArray[j]?.push(array[i][j]);
}
}
return newArray;
}
export const getChartData = (
data: TraceReducer['spansGraph']['payload'],
): ChartData<'line'> => {
const allDataPoints = data.items;
const chartDataset: ChartDatasetProperties<'line', number[]> = {
data: [],
type: 'line',
};
const chartLabels: ChartData<'line'>['labels'] = [];
if (allDataPoints && typeof allDataPoints === 'object') {
Object.keys(allDataPoints).forEach((timestamp) => {
const key = allDataPoints[timestamp];
if (key.value) {
chartDataset.data.push(key.value);
const date = dayjs(key.timestamp / 1000000);
chartLabels.push(date.toDate().getTime());
}
});
}
return {
datasets: [
{
...chartDataset,
borderWidth: 1.5,
spanGaps: true,
borderColor: colors[0] || 'red',
showLine: true,
pointRadius: 0,
},
],
labels: chartLabels,
};
};
export const getChartDataforGroupBy = (
props: TraceReducer['spansGraph']['payload'],
): ChartData => {
const { items } = props;
const chartData: ChartData = {
datasets: [],
labels: [],
};
let max = 0;
const allGroupBy = Object.keys(items).map((e) => items[e].groupBy);
keys(allGroupBy).forEach((e: string): void => {
// @ts-expect-error
const { length } = keys(allGroupBy[e]);
if (length >= max) {
max = length;
}
});
const numberOfGraphs = max;
const spansGraph: number[][] = [];
const names: string[] = [];
// number of data points
Object.keys(items).forEach((item) => {
const spanData = items[item];
const date = dayjs(Number(item) / 1000000)
.toDate()
.getTime();
chartData.labels?.push(date);
const { groupBy } = spanData;
const preData: number[] = [];
if (groupBy) {
Object.keys(groupBy).forEach((key) => {
const value = groupBy[key];
preData.push(value);
names.push(key);
});
spansGraph.push(preData);
}
});
const updatedName = [...new Set(names)];
transposeArray(spansGraph, numberOfGraphs).forEach((values, index) => {
chartData.datasets.push({
data: values.map((e) => e || 0),
borderWidth: 1.5,
spanGaps: true,
borderColor: colors[index] || 'red',
showLine: true,
pointRadius: 0,
label: updatedName[index],
});
});
return chartData;
};

View File

@@ -1,62 +0,0 @@
import { useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useMeasure } from 'react-use';
import { Typography } from '@signozhq/ui/typography';
import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import { AppState } from 'store/reducers';
import { TraceReducer } from 'types/reducer/trace';
import { getChartData, getChartDataforGroupBy } from './config';
import { Container } from './styles';
function TraceGraph(): JSX.Element {
const [ref, { width }] = useMeasure();
const { spansGraph, selectedGroupBy, yAxisUnit } = useSelector<
AppState,
TraceReducer
>((state) => state.traces);
const { loading, error, errorMessage, payload } = spansGraph;
const ChartData = useMemo(
() =>
selectedGroupBy.length === 0 || selectedGroupBy === 'none'
? getChartData(payload)
: getChartDataforGroupBy(payload),
[payload, selectedGroupBy],
);
if (error) {
return (
<Container center>
<Typography>{errorMessage || 'Something went wrong'}</Typography>
</Container>
);
}
if (loading) {
return (
<Container>
<Spinner height="20vh" size="small" tip="Loading..." />
</Container>
);
}
return (
<Container ref={ref as never}>
<Graph
animate={false}
data={ChartData}
name="traceGraph"
type="line"
yAxisUnit={yAxisUnit}
forceReRender={width}
/>
</Container>
);
}
export default TraceGraph;

View File

@@ -1,23 +0,0 @@
import { RefObject } from 'react';
import { StyledCSS } from 'container/GantChart/Trace/styles';
import styled, { css } from 'styled-components';
interface Props {
center?: boolean;
ref?: RefObject<HTMLDivElement> | null; // The ref type provided by react-use is incorrect -> https://github.com/streamich/react-use/issues/1264 Open Issue
}
export const Container = styled.div<Props>`
height: 25vh !important;
margin-top: 1rem;
margin-bottom: 1rem;
width: 100% !important;
${({ center }): StyledCSS =>
center &&
css`
display: flex;
justify-content: center;
align-items: center;
`}
`;

View File

@@ -1,97 +0,0 @@
import {
Dispatch,
SetStateAction,
useCallback,
useMemo,
useState,
} from 'react';
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Input } from '@signozhq/ui/input';
import { AutoComplete } from 'antd';
import getTagFilters from 'api/trace/getTagFilter';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { getTagKeyOptions, onTagKeySelect } from './utils';
function TagsKey(props: TagsKeysProps): JSX.Element {
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { index, setLocalSelectedTags, tag } = props;
const [selectedKey, setSelectedKey] = useState<string>(tag.Key || '');
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const { isLoading, data } = useQuery(
[
'getTagKeys',
globalTime.minTime,
globalTime.maxTime,
traces.selectedFilter,
traces.isFilterExclude,
traces.spanKind,
],
{
queryFn: () =>
getTagFilters({
start: globalTime.minTime,
end: globalTime.maxTime,
other: Object.fromEntries(traces.selectedFilter),
isFilterExclude: traces.isFilterExclude,
spanKind: traces.spanKind,
}),
cacheTime: 120000,
},
);
const options = useMemo(() => getTagKeyOptions(data?.payload), [data]);
const onSelectHandler = useCallback(
(value: unknown) =>
onTagKeySelect(
value,
options,
setSelectedKey,
setLocalSelectedTags,
index,
tag,
),
[index, options, setLocalSelectedTags, tag],
);
return (
<AutoComplete
style={{ width: '100%' }}
value={selectedKey}
allowClear
disabled={isLoading}
notFoundContent="No tags available"
showSearch
options={options?.map((e) => ({
label: e.label?.toString(),
value: e.value,
}))}
filterOption={(inputValue, option): boolean =>
option?.label?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
onChange={(e): void => setSelectedKey(e)}
onSelect={onSelectHandler}
>
<Input placeholder="Please select" />
</AutoComplete>
);
}
interface TagsKeysProps {
index: number;
tag: FlatArray<TraceReducer['selectedTags'], 1>;
setLocalSelectedTags: Dispatch<SetStateAction<TraceReducer['selectedTags']>>;
}
export default TagsKey;

View File

@@ -1,154 +0,0 @@
import {
Dispatch,
memo,
SetStateAction,
useCallback,
useMemo,
useState,
} from 'react';
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Select } from 'antd';
import type { BaseOptionType } from 'antd/es/select';
import getTagValue from 'api/trace/getTagValue';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { SelectComponent } from './styles';
import {
disableTagValue,
extractTagKey,
extractTagType,
getInitialLocalValue,
getTagValueOptions,
onTagValueChange,
selectOptions,
separateTagValues,
TagValueTypes,
} from './utils';
function TagValue(props: TagValueProps): JSX.Element {
const { tag, setLocalSelectedTags, index, tagKey } = props;
const {
Key: selectedKey,
Operator: selectedOperator,
StringValues: selectedStringValues,
NumberValues: selectedNumberValues,
BoolValues: selectedBoolValues,
} = tag;
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const [localTagValue, setLocalTagValue] = useState<TagValueTypes[]>(
getInitialLocalValue(
selectedNumberValues,
selectedBoolValues,
selectedStringValues,
),
);
const globalReducer = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const tagType = useMemo(() => extractTagType(tagKey), [tagKey]);
const { isLoading, data } = useQuery(
[
'tagKey',
globalReducer.minTime,
globalReducer.maxTime,
tagKey,
tagType,
traces.spanKind,
],
{
queryFn: () =>
getTagValue({
end: globalReducer.maxTime,
start: globalReducer.minTime,
tagKey: {
Key: extractTagKey(tagKey),
Type: tagType,
},
spanKind: traces.spanKind,
}),
},
);
const tagValueDisabled = useMemo(
() =>
disableTagValue(
selectedOperator,
setLocalTagValue,
selectedKey,
setLocalSelectedTags,
index,
),
[index, selectedKey, selectedOperator, setLocalSelectedTags],
);
const onChangeHandler = useCallback(
(value: unknown) => {
const updatedValues = onTagValueChange(value);
setLocalTagValue(updatedValues);
const { boolValues, numberValues, stringValues } = separateTagValues(
updatedValues,
selectedKey,
);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
...tags[index],
BoolValues: boolValues,
NumberValues: numberValues,
StringValues: stringValues,
},
...tags.slice(index + 1),
]);
},
[index, setLocalSelectedTags, selectedKey],
);
const getFilterOptions = useCallback(
(inputValue: string, option?: BaseOptionType): boolean => {
if (typeof option?.label === 'string') {
return option?.label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1;
}
return false;
},
[],
);
return (
<SelectComponent
loading={isLoading}
options={getTagValueOptions(data?.payload, tagType)}
mode="tags"
allowClear
showSearch
filterOption={getFilterOptions}
disabled={isLoading || tagValueDisabled}
value={localTagValue}
onChange={onChangeHandler}
>
{selectOptions(data?.payload, tagType)?.map((suggestion) => (
<Select.Option key={suggestion.toString()} value={suggestion}>
{suggestion}
</Select.Option>
))}
</SelectComponent>
);
}
interface TagValueProps {
index: number;
tag: FlatArray<TraceReducer['selectedTags'], 1>;
setLocalSelectedTags: Dispatch<SetStateAction<TraceReducer['selectedTags']>>;
tagKey: string;
}
export default memo(TagValue);

View File

@@ -1,180 +0,0 @@
import { Dispatch, SetStateAction } from 'react';
import { X } from '@signozhq/icons';
import { Select } from 'antd';
import { TraceReducer } from 'types/reducer/trace';
import { Container, IconContainer, SelectComponent } from './styles';
import TagsKey from './TagKey';
import TagValue from './TagValue';
import { mapOperators } from './utils';
const { Option } = Select;
type Tags = FlatArray<TraceReducer['selectedTags'], 1>['Operator'];
const StringBoolNumber = ['string', 'number', 'bool'];
const Number = ['number'];
const String = ['string'];
export interface AllMenuProps {
key: Tags | '';
value: string;
supportedTypes: string[];
}
export const AllMenu: AllMenuProps[] = [
{
key: 'Equals',
value: 'EQUALS',
supportedTypes: StringBoolNumber,
},
{
key: 'NotEquals',
value: 'NOT EQUALS',
supportedTypes: StringBoolNumber,
},
{
key: 'In',
value: 'IN',
supportedTypes: String,
},
{
key: 'NotIn',
value: 'NOT IN',
supportedTypes: String,
},
{
key: 'Exists',
value: 'EXISTS',
supportedTypes: StringBoolNumber,
},
{
key: 'NotExists',
value: 'NOT EXISTS',
supportedTypes: StringBoolNumber,
},
{
key: 'GreaterThan',
value: 'GREATER THAN',
supportedTypes: Number,
},
{
key: 'LessThan',
value: 'LESS THAN',
supportedTypes: Number,
},
{
key: 'GreaterThanEquals',
value: 'GREATER THAN OR EQUALS',
supportedTypes: Number,
},
{
key: 'LessThanEquals',
value: 'LESS THAN OR EQUALS',
supportedTypes: Number,
},
{
key: 'StartsWith',
value: 'STARTS WITH',
supportedTypes: String,
},
{
key: 'NotStartsWith',
value: 'NOT STARTS WITH',
supportedTypes: String,
},
{
key: 'Contains',
value: 'CONTAINS',
supportedTypes: String,
},
{
key: 'NotContains',
value: 'NOT CONTAINS',
supportedTypes: String,
},
];
function SingleTags(props: AllTagsProps): JSX.Element {
const { tag, onCloseHandler, setLocalSelectedTags, index, localSelectedTags } =
props;
const {
Key: selectedKey,
Operator: selectedOperator,
StringValues: selectedStringValues,
NumberValues: selectedNumberValues,
BoolValues: selectedBoolValues,
} = tag;
const onDeleteTagHandler = (index: number): void => {
onCloseHandler(index);
};
const onChangeOperatorHandler = (key: unknown): void => {
if (typeof key === 'string') {
setLocalSelectedTags([
...localSelectedTags.slice(0, index),
{
Key: selectedKey,
StringValues: selectedStringValues,
NumberValues: selectedNumberValues,
BoolValues: selectedBoolValues,
Operator: key as Tags,
},
...localSelectedTags.slice(index + 1, localSelectedTags.length),
]);
}
};
return (
<Container>
<TagsKey
index={index}
tag={tag}
setLocalSelectedTags={setLocalSelectedTags}
/>
<SelectComponent
onChange={onChangeOperatorHandler}
value={AllMenu.find((e) => e.key === selectedOperator)?.value || ''}
>
{
// filter out the operator that does not include supported type of the selected key
mapOperators(selectedKey).map((e) => (
<Option key={e.value} value={e.key}>
{e.value}
</Option>
))
}
</SelectComponent>
{selectedKey ? (
<TagValue
index={index}
tag={tag}
setLocalSelectedTags={setLocalSelectedTags}
tagKey={selectedKey}
/>
) : (
<SelectComponent />
)}
<IconContainer role="button" onClick={(): void => onDeleteTagHandler(index)}>
<X size="md" />
</IconContainer>
</Container>
);
}
interface AllTagsProps {
onCloseHandler: (index: number) => void;
index: number;
tag: FlatArray<TraceReducer['selectedTags'], 1>;
setLocalSelectedTags: Dispatch<SetStateAction<TraceReducer['selectedTags']>>;
localSelectedTags: TraceReducer['selectedTags'];
}
export interface Value {
key: string;
label: string;
value: string;
}
export default SingleTags;

View File

@@ -1,38 +0,0 @@
import { Select, Space } from 'antd';
import styled from 'styled-components';
export const SpaceComponent = styled(Space)`
&&& {
width: 100%;
}
`;
export const Container = styled(Space)`
&&& {
display: flex;
margin-top: 1rem;
margin-bottom: 1rem;
}
.ant-space-item:not(:last-child, :nth-child(2)) {
width: 100%;
}
.ant-space-item:nth-child(2) {
width: 50%;
}
`;
export const IconContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
margin-left: 1.125rem;
`;
export const SelectComponent = styled(Select)`
&&& {
width: 100%;
}
`;

View File

@@ -1,232 +0,0 @@
import { Dispatch, SetStateAction } from 'react';
import { AutoCompleteProps } from 'antd';
import type { DefaultOptionType } from 'antd/es/select';
import { PayloadProps as TagKeyPayload } from 'types/api/trace/getTagFilters';
import { PayloadProps as TagValuePayload } from 'types/api/trace/getTagValue';
import { OperatorValues, Tags } from 'types/reducer/trace';
import { AllMenu, AllMenuProps } from '.';
export type TagValueTypes = string | number | boolean;
/**
* @description extract tag filters from payload
*/
export const extractTagFilters = (
payload: TagKeyPayload,
): DefaultOptionType[] => {
const tagFilters: string[] = [];
payload.stringTagKeys.forEach((element) => {
tagFilters.push(`${element}.(string)`);
});
payload.numberTagKeys.forEach((element) => {
tagFilters.push(`${element}.(number)`);
});
payload.boolTagKeys.forEach((element) => {
tagFilters.push(`${element}.(bool)`);
});
return tagFilters.map((e) => ({
value: e,
label: e,
}));
};
export const extractTagType = (tagKey: string): string => {
if (tagKey?.includes('.(string)')) {
return 'string';
}
if (tagKey?.includes('.(number)')) {
return 'number';
}
if (tagKey?.includes('.(bool)')) {
return 'bool';
}
return 'string';
};
export const extractTagKey = (tagKey: string): string => {
const tag = tagKey.split('.(');
if (tag && tag.length > 0) {
return tag[0];
}
return '';
};
export function onTagValueChange(values: unknown): TagValueTypes[] {
const stringValues = values as string[];
if (!Array.isArray(stringValues) || stringValues.length === 0) {
return [];
}
return values as TagValueTypes[];
}
export function separateTagValues(
values: TagValueTypes[],
selectedKey: string,
): { boolValues: boolean[]; numberValues: number[]; stringValues: string[] } {
if (selectedKey.includes('.(bool)')) {
const boolValues = values.filter(
(value) => typeof value === 'boolean',
) as boolean[];
return {
boolValues,
numberValues: [],
stringValues: [],
};
}
if (selectedKey.includes('.(number)')) {
const numberValues = values
.filter((value) => typeof value === 'number' || !Number.isNaN(Number(value)))
.map((value) => Number(value)) as number[];
return {
boolValues: [],
numberValues,
stringValues: [],
};
}
const stringValues = values.filter(
(value) =>
typeof value === 'string' &&
value !== 'true' &&
value !== 'false' &&
Number.isNaN(Number(value)),
) as string[];
return {
boolValues: [],
numberValues: [],
stringValues,
};
}
export function disableTagValue(
selectedOperator: OperatorValues,
setLocalValue: Dispatch<SetStateAction<TagValueTypes[]>>,
selectedKeys: string,
setLocalSelectedTags: Dispatch<SetStateAction<Tags[]>>,
index: number,
): boolean {
if (selectedOperator === 'Exists' || selectedOperator === 'NotExists') {
setLocalValue([]);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: selectedKeys,
Operator: selectedOperator,
StringValues: [],
NumberValues: [],
BoolValues: [],
},
...tags.slice(index + 1, tags.length),
]);
return true;
}
return false;
}
export function getInitialLocalValue(
selectedNumberValues: number[],
selectedBoolValues: boolean[],
selectedStringValues: string[],
): TagValueTypes[] {
return [
...selectedBoolValues,
...selectedNumberValues,
...selectedStringValues,
];
}
export function getTagValueOptions(
payload: TagValuePayload | null | undefined,
tagType: string,
): Array<{ label: string; value: TagValueTypes }> | undefined {
if (tagType === 'string') {
return payload?.stringTagValues?.map((e) => ({
label: e,
value: e,
}));
}
if (tagType === 'number') {
return payload?.numberTagValues?.map((e) => ({
label: e.toString(),
value: e,
}));
}
if (tagType === 'bool') {
return payload?.boolTagValues?.map((e) => ({
label: e.toString(),
value: e,
}));
}
return [];
}
export function getTagKeyOptions(
payload: TagKeyPayload | null | undefined,
): DefaultOptionType[] {
if (payload === null) {
return [
{
value: '',
label: 'No tags available',
},
];
}
if (payload != null) {
return extractTagFilters(payload);
}
return [];
}
export function selectOptions(
payload: TagValuePayload | null | undefined,
tagType: string,
): string[] | boolean[] | number[] | undefined {
if (tagType === 'string') {
return payload?.stringTagValues;
}
if (tagType === 'number') {
return payload?.numberTagValues;
}
if (tagType === 'bool') {
return payload?.boolTagValues;
}
return [];
}
export function mapOperators(selectedKey: string): AllMenuProps[] {
return AllMenu.filter((e) =>
e?.supportedTypes?.includes(extractTagType(selectedKey)),
);
}
export function onTagKeySelect(
value: unknown,
options: AutoCompleteProps['options'],
setSelectedKey: Dispatch<SetStateAction<string>>,
setLocalSelectedTags: Dispatch<SetStateAction<Tags[]>>,
index: number,
tag: Tags,
): void {
if (
typeof value === 'string' &&
options &&
options.find((option) => option.value === value)
) {
setSelectedKey(value);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: value,
Operator: tag.Operator,
StringValues: tag.StringValues,
NumberValues: tag.NumberValues,
BoolValues: tag.BoolValues,
},
...tags.slice(index + 1, tags.length),
]);
}
}

View File

@@ -1,153 +0,0 @@
import { useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { connect, useSelector } from 'react-redux';
import { ChevronRight, Plus } from '@signozhq/icons';
import { Button, Space, Flex } from 'antd';
import { Typography } from '@signozhq/ui/typography';
// eslint-disable-next-line no-restricted-imports
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { UpdateTagIsError } from 'store/actions/trace/updateIsTagsError';
import { UpdateTagVisibility } from 'store/actions/trace/updateTagPanelVisiblity';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { TraceReducer } from 'types/reducer/trace';
import { parseTagsToQuery } from '../util';
import {
ButtonContainer,
Container,
CurrentTagsContainer,
ErrorContainer,
} from './styles';
import Tags from './Tag';
const { Text } = Typography;
function AllTags({
updateTagIsError,
onChangeHandler,
updateTagVisibility,
updateFilters,
}: AllTagsProps): JSX.Element {
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const [localSelectedTags, setLocalSelectedTags] = useState<
TraceReducer['selectedTags']
>(traces.selectedTags);
const onTagAddHandler = (): void => {
setLocalSelectedTags((tags) => [
...tags,
{
Key: '',
Operator: 'Equals',
StringValues: [],
NumberValues: [],
BoolValues: [],
},
]);
};
const onCloseHandler = (index: number): void => {
setLocalSelectedTags([
...localSelectedTags.slice(0, index),
...localSelectedTags.slice(index + 1, localSelectedTags.length),
]);
};
const onRunQueryHandler = (): void => {
const parsedQuery = parseTagsToQuery(localSelectedTags);
if (parsedQuery.isError) {
updateTagIsError(true);
} else {
onChangeHandler(parsedQuery.payload);
updateFilters(localSelectedTags);
updateTagIsError(false);
updateTagVisibility(false);
}
};
const onResetHandler = (): void => {
setLocalSelectedTags([]);
};
if (traces.isTagModalError) {
return (
<ErrorContainer>
<Text style={{ color: 'var(--warning-background)' }}>
Unrecognized query format. Please reset your query by clicking `X` in the
search bar above.
</Text>
<Text style={{ color: 'var(--warning-background)' }}>
Please click on the search bar to get a drop down to select relevant tags
</Text>
</ErrorContainer>
);
}
return (
<Container>
<Typography>Tags</Typography>
<CurrentTagsContainer>
{localSelectedTags.map((tags, index) => (
<Tags
key={tags.Key}
tag={tags}
index={index}
onCloseHandler={(): void => onCloseHandler(index)}
setLocalSelectedTags={setLocalSelectedTags}
localSelectedTags={localSelectedTags}
/>
))}
</CurrentTagsContainer>
<Space wrap direction="horizontal">
<Button type="primary" onClick={onTagAddHandler}>
<Flex gap={4} align="center">
<Plus size="md" />
Add Tags Filter
</Flex>
</Button>
<Text truncate={1}>
Results will include spans with ALL the specified tags ( Rows are `ANDed` )
</Text>
</Space>
<ButtonContainer>
<Space align="start">
<Button onClick={onResetHandler}>Reset</Button>
<Button type="primary" onClick={onRunQueryHandler}>
<Flex gap={4} align="center">
<ChevronRight size="md" />
Run Query
</Flex>
</Button>
</Space>
</ButtonContainer>
</Container>
);
}
interface DispatchProps {
updateTagIsError: (value: boolean) => void;
updateTagVisibility: (value: boolean) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateTagIsError: bindActionCreators(UpdateTagIsError, dispatch),
updateTagVisibility: bindActionCreators(UpdateTagVisibility, dispatch),
});
interface AllTagsProps extends DispatchProps {
updateFilters: (tags: TraceReducer['selectedTags']) => void;
onChangeHandler: (search: string) => void;
}
export default connect(null, mapDispatchToProps)(AllTags);

View File

@@ -1,39 +0,0 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const Container = styled(Card)`
min-height: 20vh;
width: 100%;
min-width: 81.5vw;
`;
export const ErrorContainer = styled(Card)`
min-height: 20vh;
width: 100%;
z-index: 2;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
`;
export const ButtonContainer = styled(Card)`
display: flex;
justify-content: flex-end;
align-items: center;
padding-top: 11px !important;
padding-bottom: 11px !important;
padding-right: 38.01px !important;
margin-top: 1rem !important;
.ant-card-body {
padding: 0;
}
`;
export const CurrentTagsContainer = styled.div`
margin-bottom: 1rem;
`;

View File

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

View File

@@ -1,152 +0,0 @@
import { useEffect, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { connect, useDispatch, useSelector } from 'react-redux';
import { ChevronRight } from '@signozhq/icons';
import { Flex, Popover } from 'antd';
// eslint-disable-next-line no-restricted-imports
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { UpdateTagIsError } from 'store/actions/trace/updateIsTagsError';
import { UpdateTagVisibility } from 'store/actions/trace/updateTagPanelVisiblity';
import { 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 { TraceReducer } from 'types/reducer/trace';
import Tags from './AllTags';
import { contentStyle } from './config';
import { Container, SearchComponent } from './styles';
import { parseQueryToTags, parseTagsToQuery } from './util';
function Search({
updateTagVisibility,
updateTagIsError,
}: SearchProps): JSX.Element {
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const [value, setValue] = useState<string>('');
const dispatch = useDispatch<Dispatch<AppActions>>();
useEffect(() => {
if (!traces.filterLoading) {
const initialTags = parseTagsToQuery(traces.selectedTags);
if (!initialTags.isError) {
setValue(initialTags.payload);
}
}
}, [traces.selectedTags, traces.filterLoading]);
useEffect(() => {
if (value.length === 0 && traces.isTagModalError) {
updateTagIsError(false);
}
}, [traces.isTagModalError, value, updateTagIsError]);
const tagRef = useRef<HTMLDivElement>(null);
const onChangeHandler = (search: string): void => {
setValue(search);
};
const setIsTagsModalHandler = (value: boolean): void => {
updateTagVisibility(value);
};
const updateFilters = async (
selectedTags: TraceReducer['selectedTags'],
): Promise<void> => {
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
selectedTags,
current: traces.spansAggregate.currentPage,
filter: traces.filter,
filterToFetchData: traces.filterToFetchData,
selectedFilter: traces.selectedFilter,
userSelected: traces.userSelectedFilter,
isFilterExclude: traces.isFilterExclude,
order: traces.spansAggregate.order,
pageSize: traces.spansAggregate.pageSize,
orderParam: traces.spansAggregate.orderParam,
spanKind: traces.spanKind,
},
});
updateURL(
traces.selectedFilter,
traces.filterToFetchData,
traces.spansAggregate.currentPage,
selectedTags,
traces.isFilterExclude,
traces.userSelectedFilter,
traces.spansAggregate.order,
traces.spansAggregate.pageSize,
traces.spansAggregate.orderParam,
);
};
return (
<Container ref={tagRef}>
<Popover
placement="bottomLeft"
destroyTooltipOnHide
open={traces.isTagModalOpen}
trigger="click"
onOpenChange={setIsTagsModalHandler}
showArrow={false}
overlayInnerStyle={contentStyle}
content={
<Tags updateFilters={updateFilters} onChangeHandler={onChangeHandler} />
}
>
<SearchComponent
onChange={(event): void => onChangeHandler(event.target.value)}
value={value}
allowClear
disabled={traces.filterLoading}
placeholder="Click to filter by tags"
type="search"
enterButton={
<Flex align="center">
<ChevronRight size="md" />
</Flex>
}
onSearch={(string): void => {
if (string.length === 0) {
updateTagVisibility(false);
updateFilters([]);
return;
}
const { isError, payload } = parseQueryToTags(string);
if (isError) {
updateTagIsError(true);
} else {
updateTagIsError(false);
updateTagVisibility(false);
updateFilters(payload);
}
}}
/>
</Popover>
</Container>
);
}
interface DispatchProps {
updateTagVisibility: (value: boolean) => void;
updateTagIsError: (value: boolean) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateTagVisibility: bindActionCreators(UpdateTagVisibility, dispatch),
updateTagIsError: bindActionCreators(UpdateTagIsError, dispatch),
});
type SearchProps = DispatchProps;
export default connect(null, mapDispatchToProps)(Search);

View File

@@ -1,19 +0,0 @@
import { Input } from 'antd';
import styled from 'styled-components';
const { Search } = Input;
export const Container = styled.div`
display: flex;
position: relative;
width: 100%;
margin-top: 1rem;
`;
export const SearchComponent = styled(Search)`
.ant-btn-primary {
svg {
transform: scale(1.5);
}
}
`;

View File

@@ -1,158 +0,0 @@
import { AllMenu } from 'container/Trace/Search/AllTags/Tag';
import { TraceReducer } from 'types/reducer/trace';
import { extractTagType, TagValueTypes } from './AllTags/Tag/utils';
type Tags = TraceReducer['selectedTags'];
interface PayloadProps<T> {
isError: boolean;
payload: T;
}
function extractValues(
tagType: string,
filters: string[],
isError: boolean,
): [number[], boolean[], string[], boolean] {
const StringValues: string[] = [];
const NumberValues: number[] = [];
const BoolValues: boolean[] = [];
let isErr = isError;
if (tagType === 'string') {
StringValues.push(...filters);
} else if (tagType === 'number') {
filters.forEach((element) => {
const num = Number(element);
isErr = Number.isNaN(num) ? true : isError;
NumberValues.push(num);
});
} else if (tagType === 'bool') {
filters.forEach((element) => {
if (element === 'true') {
BoolValues.push(true);
} else if (element === 'false') {
BoolValues.push(false);
} else {
isErr = true;
}
});
}
return [NumberValues, BoolValues, StringValues, isErr];
}
export const parseQueryToTags = (query: string): PayloadProps<Tags> => {
let isError = false;
// Split the query string by ' AND '
const noOfTags = query.split(' AND ');
// Map over each tag
const tags: Tags = noOfTags.map((filter) => {
// Find the operator used in the filter
const operator =
AllMenu.find((e) => `${filter} `.includes(` ${e.key} `))?.key || '';
// Split the filter by the operator
const [tagName, tagValues] = filter.split(operator).map((e) => e.trim());
// If the operator is Exists or NotExists, then return the tag object without values
if (operator === 'Exists' || operator === 'NotExists') {
return {
Key: tagName,
StringValues: [],
NumberValues: [],
BoolValues: [],
Operator: operator as FlatArray<Tags, 1>['Operator'],
};
}
// Check for errors in the filter
isError = operator.length === 0 || !tagName || !tagValues ? true : isError;
// Remove the first and last brackets from the tagValues
const formattedTagValues = tagValues.slice(1, -1);
// Split the tagValues by ',' and remove any quotes
const filters = formattedTagValues
.split(',')
.map((e) => e.replaceAll(/"/g, ''));
// Check for errors in the filters
filters.forEach((e) => {
const firstChar = e.charAt(0);
const lastChar = e.charAt(e.length - 1);
isError = firstChar === '"' && lastChar === '"' ? true : isError;
});
// Extract the tag type
const tagType = extractTagType(tagName);
// Extract the values for the tag
const [NumberValues, BoolValues, StringValues, isErr] = extractValues(
tagType,
filters,
isError,
);
isError = isErr;
// Return the tag object
return {
Key: tagName,
StringValues,
NumberValues,
BoolValues,
Operator: operator as FlatArray<Tags, 1>['Operator'],
};
});
return {
isError,
payload: tags,
};
};
const formatValues = (values: TagValueTypes[]): string =>
values.map((e) => `"${e.toString().replaceAll(/"/g, '')}"`).join(',');
export const parseTagsToQuery = (tags: Tags): PayloadProps<string> => {
let isError = false;
// Map over each tag
const payload = tags
.map(({ StringValues, NumberValues, BoolValues, Key, Operator }) => {
// Check if the key of the tag is undefined
if (!Key) {
isError = true;
}
if (Operator === 'Exists' || Operator === 'NotExists') {
return `${Key} ${Operator}`;
}
// Check if the tag has string values
if (StringValues.length > 0) {
// Format the string values and join them with a ','
const formattedStringValues = formatValues(StringValues);
return `${Key} ${Operator} (${formattedStringValues})`;
}
// Check if the tag has number values
if (NumberValues.length > 0) {
// Format the number values and join them with a ','
const formattedNumberValues = formatValues(NumberValues);
return `${Key} ${Operator} (${formattedNumberValues})`;
}
// Check if the tag has boolean values
if (BoolValues.length > 0) {
// Format the boolean values and join them with a ','
const formattedBoolValues = formatValues(BoolValues);
return `${Key} ${Operator} (${formattedBoolValues})`;
}
return '';
})
.join(' AND ');
return {
isError,
payload,
};
};

View File

@@ -1,107 +0,0 @@
import type { DefaultOptionType } from 'antd/es/select';
interface Dropdown {
key: string;
displayValue: string;
yAxisUnit?: string;
}
export const groupBy: DefaultOptionType[] = [
{
label: 'None',
value: 'none',
},
{
label: 'Service Name',
value: 'serviceName',
},
{
label: 'Operation',
value: 'name',
},
{
label: 'HTTP URL',
value: 'httpUrl',
},
{
label: 'HTTP Method',
value: 'httpMethod',
},
{
label: 'HTTP Host',
value: 'httpHost',
},
{
label: 'HTTP Route',
value: 'httpRoute',
},
{
label: 'RPC Method',
value: 'rpcMethod',
},
{
label: 'Status Code',
value: 'responseStatusCode',
},
{
label: 'Database Name',
value: 'dbName',
},
{
label: 'Database System',
value: 'dbSystem',
},
{
label: 'Database Operation',
value: 'dbOperation',
},
{
label: 'Messaging System',
value: 'msgSystem',
},
{
label: 'Messaging Operation',
value: 'msgOperation',
},
];
export const functions: Dropdown[] = [
{ displayValue: 'Count', key: 'count', yAxisUnit: 'short' },
{
displayValue: 'Rate per sec',
key: 'ratePerSec',
yAxisUnit: 'reqps',
},
{ displayValue: 'Sum (duration)', key: 'sum', yAxisUnit: 'ns' },
{ displayValue: 'Avg (duration)', key: 'avg', yAxisUnit: 'ns' },
{
displayValue: 'Max (duration)',
key: 'max',
yAxisUnit: 'ns',
},
{
displayValue: 'Min (duration)',
key: 'min',
yAxisUnit: 'ns',
},
{
displayValue: '50th percentile (duration)',
key: 'p50',
yAxisUnit: 'ns',
},
{
displayValue: '90th percentile (duration)',
key: 'p90',
yAxisUnit: 'ns',
},
{
displayValue: '95th percentile (duration)',
key: 'p95',
yAxisUnit: 'ns',
},
{
displayValue: '99th percentile (duration)',
key: 'p99',
yAxisUnit: 'ns',
},
];

View File

@@ -1,102 +0,0 @@
import { 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, Space } 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 { popupContainer } from 'utils/selectPopupContainer';
import { functions } from './config';
import { SelectComponent } from './styles';
import {
filterGroupBy,
getSelectedValue,
initOptions,
onClickSelectedFunctionHandler,
onClickSelectedGroupByHandler,
selectedGroupByValue,
} from './utils';
const { Option } = SelectComponent;
function TraceGraphFilter(): JSX.Element {
const { selectedFunction, selectedGroupBy } = useSelector<
AppState,
TraceReducer
>((state) => state.traces);
const [selectedGroupByLocal, setSelectedGroupByLocal] =
useState<string>(selectedGroupBy);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
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,
enabled: traces.filter.size > 0,
},
);
const options = useMemo(() => initOptions(data?.payload), [data?.payload]);
return (
<Space>
<label htmlFor="selectedFunction">Function</label>
<SelectComponent
getPopupContainer={popupContainer}
dropdownMatchSelectWidth
data-testid="selectedFunction"
id="selectedFunction"
value={getSelectedValue(selectedFunction)}
onChange={onClickSelectedFunctionHandler}
>
{functions.map((value) => (
<Option value={value.key} key={value.key}>
{value.displayValue}
</Option>
))}
</SelectComponent>
<label htmlFor="selectedGroupBy">Group By</label>
<AutoComplete
getPopupContainer={popupContainer}
dropdownMatchSelectWidth
id="selectedGroupBy"
data-testid="selectedGroupBy"
options={options}
value={selectedGroupByValue(selectedGroupByLocal, options)}
onChange={(e): void => setSelectedGroupByLocal(e.toString())}
onSelect={onClickSelectedGroupByHandler(options)}
filterOption={(inputValue, option): boolean =>
filterGroupBy(inputValue, option)
}
>
<Input disabled={isLoading} placeholder="Please select" />
</AutoComplete>
</Space>
);
}
export default TraceGraphFilter;

View File

@@ -1,8 +0,0 @@
import { Select } from 'antd';
import styled from 'styled-components';
export const SelectComponent = styled(Select)`
&&& {
min-width: 12rem;
}
`;

View File

@@ -1,27 +0,0 @@
import { selectedGroupByValue } from './utils';
const options = [
{
value: '1',
label: '1',
},
{
value: '2',
label: '2',
},
];
describe('TraceGraphFilter/utils', () => {
it('should return the correct value', () => {
const selectedGroupBy = '1';
const result = selectedGroupByValue(selectedGroupBy, options);
expect(result).toStrictEqual(selectedGroupBy);
});
it('should return the correct value when selectedOption not found', () => {
const selectedGroupBy = '3';
const result = selectedGroupByValue(selectedGroupBy, options);
expect(result).toStrictEqual(selectedGroupBy);
});
});

View File

@@ -1,88 +0,0 @@
import { ReactNode } from 'react';
import type { DefaultOptionType } from 'antd/es/select';
import store from 'store';
import {
UPDATE_SELECTED_FUNCTION,
UPDATE_SELECTED_GROUP_BY,
} from 'types/actions/trace';
import { PayloadProps } from 'types/api/trace/getTagFilters';
import { extractTagFilters } from '../Search/AllTags/Tag/utils';
import { functions, groupBy } from './config';
export function groupByValues(
tagFilters: DefaultOptionType[],
): DefaultOptionType[] {
const result: DefaultOptionType[] = [...groupBy];
tagFilters.forEach((e) => {
result.push(e);
});
return result;
}
export function initOptions(
payload: PayloadProps | null | undefined,
): DefaultOptionType[] {
if (payload) {
return groupByValues(extractTagFilters(payload));
}
return groupBy;
}
export function onClickSelectedGroupByHandler(options: DefaultOptionType[]) {
return (ev: unknown): void => {
const { dispatch } = store;
if (typeof ev === 'string' && options) {
const selected = options.find((e) => e.value === ev);
if (selected) {
dispatch({
type: UPDATE_SELECTED_GROUP_BY,
payload: {
selectedGroupBy: selected.value ? selected.value.toString() : '',
},
});
}
}
};
}
export function onClickSelectedFunctionHandler(value: unknown): void {
const { dispatch } = store;
if (typeof value === 'string') {
const selected = functions.find((e) => e.key === value);
if (selected) {
dispatch({
type: UPDATE_SELECTED_FUNCTION,
payload: {
selectedFunction: selected.key,
yAxisUnit: selected.yAxisUnit,
},
});
}
}
}
export function selectedGroupByValue(
selectedGroupBy: string,
options: DefaultOptionType[],
): ReactNode {
const optionValue = options.find((e) => selectedGroupBy === e.value)?.label;
if (optionValue) {
return optionValue;
}
return selectedGroupBy;
}
export function getSelectedValue(selectedFunction: string): unknown {
return functions.find((e) => selectedFunction === e.key)?.displayValue;
}
export function filterGroupBy(
inputValue: string,
option: DefaultOptionType | undefined,
): boolean {
return (
option?.label?.toString().toUpperCase().indexOf(inputValue.toUpperCase()) !==
-1
);
}

View File

@@ -1,232 +0,0 @@
import { HTMLAttributes } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { TableColumnsType as ColumnsType, TableProps } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { Typography } from '@signozhq/ui/typography';
import { ResizeTable } from 'components/ResizeTable';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import ROUTES from 'constants/routes';
import {
getSpanOrder,
getSpanOrderParam,
} from 'container/Trace/TraceTable/util';
import { formUrlParams } from 'container/TraceDetail/utils';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import history from 'lib/history';
import omit from 'lodash-es/omit';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { updateURL } from 'store/actions/trace/util';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
UPDATE_SPAN_ORDER,
UPDATE_SPAN_ORDER_PARAMS,
UPDATE_SPANS_AGGREGATE_PAGE_NUMBER,
UPDATE_SPANS_AGGREGATE_PAGE_SIZE,
} from 'types/actions/trace';
import { TraceReducer } from 'types/reducer/trace';
import { openInNewTab } from 'utils/navigation';
import { v4 } from 'uuid';
dayjs.extend(duration);
function TraceTable(): JSX.Element {
const {
spansAggregate,
selectedFilter,
selectedTags,
filterLoading,
userSelectedFilter,
isFilterExclude,
filterToFetchData,
filter,
} = useSelector<AppState, TraceReducer>((state) => state.traces);
const statusFilter = filter.get('status');
const selectedStatusFilter = selectedFilter.get('status');
const dispatch = useDispatch<Dispatch<AppActions>>();
const { loading, order: spansAggregateOrder } = spansAggregate;
type TableType = FlatArray<TraceReducer['spansAggregate']['data'], 1>;
const getLink = (record: TableType): string =>
`${ROUTES.TRACE}/${record.traceID}${formUrlParams({
spanId: record.spanID,
levelUp: 0,
levelDown: 0,
})}`;
const getValue = (value: string): JSX.Element => (
<Typography>{value}</Typography>
);
const getHttpMethodOrStatus = (
value: TableType['statusCode'],
): JSX.Element => {
if (value.length === 0) {
return <Typography>-</Typography>;
}
return <Badge color="sakura">{value}</Badge>;
};
const columns: ColumnsType<TableType> = [
{
title: 'Date',
dataIndex: 'timestamp',
key: 'timestamp',
width: 120,
sorter: true,
render: (value: TableType['timestamp']): JSX.Element => {
const day = dayjs(value);
return (
<Typography>
{day.format(DATE_TIME_FORMATS.SLASH_DATETIME_SECONDS)}
</Typography>
);
},
},
{
title: 'Service',
dataIndex: 'serviceName',
key: 'serviceName',
width: 50,
render: getValue,
},
{
title: 'Operation',
dataIndex: 'operation',
key: 'operation',
width: 110,
render: getValue,
},
{
title: 'Duration',
dataIndex: 'durationNano',
key: 'durationNano',
width: 50,
sorter: true,
render: (value: TableType['durationNano']): JSX.Element => (
<Typography>
{`${dayjs
.duration({ milliseconds: value / 1000000 })
.asMilliseconds()
.toFixed(2)} ms`}
</Typography>
),
},
{
title: 'Method',
dataIndex: 'method',
key: 'method',
width: 50,
render: getHttpMethodOrStatus,
},
{
title: 'Status Code',
dataIndex: 'statusCode',
key: 'statusCode',
width: 50,
render: getHttpMethodOrStatus,
},
];
const onChangeHandler: TableProps<TableType>['onChange'] = (
props,
_,
sort,
) => {
if (!Array.isArray(sort)) {
const { order = spansAggregateOrder } = sort;
if (props.current && props.pageSize) {
const spanOrder = getSpanOrder(order || '');
const orderParam = getSpanOrderParam(sort.field as string);
dispatch({
type: UPDATE_SPAN_ORDER,
payload: {
order: spanOrder,
},
});
dispatch({
type: UPDATE_SPAN_ORDER_PARAMS,
payload: {
orderParam,
},
});
dispatch({
type: UPDATE_SPANS_AGGREGATE_PAGE_SIZE,
payload: {
pageSize: props.pageSize,
},
});
dispatch({
type: UPDATE_SPANS_AGGREGATE_PAGE_NUMBER,
payload: {
currentPage: props.current,
},
});
updateURL(
selectedFilter,
filterToFetchData,
props.current,
selectedTags,
isFilterExclude,
userSelectedFilter,
spanOrder,
props.pageSize,
orderParam,
);
}
}
};
const totalObject = omit(statusFilter, [...(selectedStatusFilter || [])]);
const totalCount = Object.values(totalObject).reduce(
(a, b) => parseInt(String(a), 10) + parseInt(String(b), 10),
0,
) as number;
return (
<ResizeTable
columns={columns}
onChange={onChangeHandler}
dataSource={spansAggregate.data}
loading={loading || filterLoading}
rowKey={(record: { traceID: string; spanID: string }): string =>
`${record.traceID}-${record.spanID}-${v4()}`
}
style={{
cursor: 'pointer',
}}
onRow={(record: TableType): HTMLAttributes<TableType> => ({
onClick: (event): void => {
event.preventDefault();
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
openInNewTab(getLink(record));
} else {
history.push(getLink(record));
}
},
})}
pagination={{
current: spansAggregate.currentPage,
pageSize: spansAggregate.pageSize,
responsive: true,
position: ['bottomLeft'],
total: totalCount,
}}
/>
);
}
export default TraceTable;

View File

@@ -1,19 +0,0 @@
export const getSpanOrderParam = (key: string): string => {
if (key === 'durationNano' || key === 'duration_nano') {
return 'duration';
}
if (key === 'timestamp') {
return 'timestamp';
}
return '';
};
export const getSpanOrder = (order: string): string => {
if (order === 'ascend') {
return 'ascending';
}
if (order === 'descend') {
return 'descending';
}
return '';
};

View File

@@ -1,15 +0,0 @@
.tagTooltip {
.ant-tooltip-content {
max-height: 300px;
overflow-x: hidden;
overflow-y: auto;
padding: 8px;
background-color: var(--l1-border);
margin-bottom: 8px;
}
.ant-tooltip-inner {
box-shadow: none;
}
}

View File

@@ -1,59 +0,0 @@
import { ITraceTree } from 'types/api/trace/getTraceItem';
import { getTreeLevelsCount } from './utils';
describe('traces/getTreeLevelsCount', () => {
const createNode = (id: string, children: ITraceTree[] = []): ITraceTree => ({
id,
name: '',
value: 0,
time: 0,
startTime: 0,
tags: [],
children,
serviceName: '',
serviceColour: '',
spanKind: '',
statusCodeString: '',
statusMessage: '',
});
it('should return 0 for empty tree', () => {
const emptyTree = null;
expect(getTreeLevelsCount(emptyTree as unknown as ITraceTree)).toBe(0);
});
it('should return 1 for a tree with a single node', () => {
const singleNodeTree = createNode('1');
expect(getTreeLevelsCount(singleNodeTree)).toBe(1);
});
it('should return correct depth for a balanced tree', () => {
const tree = createNode('1', [
createNode('2', [createNode('4'), createNode('5')]),
createNode('3', [createNode('6'), createNode('7')]),
]);
expect(getTreeLevelsCount(tree)).toBe(3);
});
it('should return correct depth for an unbalanced tree', () => {
const tree = createNode('1', [
createNode('2', [
createNode('4', [createNode('8', [createNode('11')])]),
createNode('5'),
]),
createNode('3', [createNode('6'), createNode('7', [createNode('10')])]),
]);
expect(getTreeLevelsCount(tree)).toBe(5);
});
it('should return correct depth for a tree with single child nodes', () => {
const tree = createNode('1', [
createNode('2', [createNode('3', [createNode('4', [createNode('5')])])]),
]);
expect(getTreeLevelsCount(tree)).toBe(5);
});
});

View File

@@ -1,50 +0,0 @@
import { useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { render, renderHook } from '@testing-library/react';
import TraceFlameGraph from 'container/TraceFlameGraph';
import store from 'store';
it('loads and displays greeting', () => {
const { rerender } = renderHook(() => useState(''));
const { asFragment } = render(
<Provider store={store}>
<TraceFlameGraph
{...{
hoveredSpanId: '',
intervalUnit: { multiplier: 0, name: 'm' },
onSpanHover: rerender,
onSpanSelect: (): void => {},
selectedSpanId: '',
traceMetaData: {
globalEnd: 0,
globalStart: 0,
levels: 0,
spread: 0,
totalSpans: 0,
},
missingSpanTree: false,
treeData: {
children: [],
id: '',
name: '',
serviceColour: '#a0e',
serviceName: '',
startTime: 0,
tags: [],
time: 100,
value: 100,
event: [],
hasError: false,
parent: undefined,
spanKind: '',
statusCodeString: '',
statusMessage: '',
},
}}
/>
</Provider>,
);
expect(asFragment()).toMatchSnapshot();
});

View File

@@ -1,34 +0,0 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`loads and displays greeting 1`] = `
<DocumentFragment>
.c1 {
position: absolute;
top: 0px;
left: NaN%;
width: Infinity%;
height: 10px;
margin: 1px 0;
background-color: hsl(282.9,100%,60.7%);
border-radius: 5px;
z-index: 1;
}
.c0 {
position: relative;
width: 100%;
height: 120px;
}
<div
class="c0"
height="0"
>
<div
class="c1"
title=""
width="Infinity"
/>
</div>
</DocumentFragment>
`;

View File

@@ -1,184 +0,0 @@
import {
Dispatch,
SetStateAction,
useLayoutEffect,
useMemo,
useState,
} from 'react';
import Color from 'color';
import { ITraceMetaData } from 'container/GantChart';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ITraceTree } from 'types/api/trace/getTraceItem';
import {
SpanItemContainer,
TOTAL_SPAN_HEIGHT,
TraceFlameGraphContainer,
} from './styles';
interface SpanItemProps {
topOffset: number;
leftOffset: number;
width: number;
spanData: ITraceTree;
tooltipText: string;
onSpanSelect: (id: string) => void;
onSpanHover: Dispatch<SetStateAction<string>>;
hoveredSpanId: string;
selectedSpanId: string;
}
function SpanItem({
topOffset = 0, // top offset in px
leftOffset = 0, // left offset in %
width = 10, // width in %
spanData,
tooltipText,
onSpanSelect, // function which gets invoked on clicking span
onSpanHover,
hoveredSpanId,
selectedSpanId,
}: SpanItemProps): JSX.Element {
const { serviceColour } = spanData;
const [isSelected, setIsSelected] = useState<boolean>(false);
// const [isLocalHover, setIsLocalHover] = useState<boolean>(false);
const isDarkMode = useIsDarkMode();
useLayoutEffect(() => {
if (
!isSelected &&
(spanData.id === hoveredSpanId || spanData.id === selectedSpanId)
) {
setIsSelected(true);
}
}, [hoveredSpanId, selectedSpanId, isSelected, spanData]);
const handleHover = (hoverState: boolean): void => {
// setIsLocalHover(hoverState);
if (hoverState) {
onSpanHover(spanData.id);
} else {
onSpanHover('');
}
};
const handleClick = (): void => {
onSpanSelect(spanData.id);
};
const spanColor = useMemo((): string => {
const selectedSpanColor = isDarkMode
? Color(serviceColour).lighten(0.3)
: Color(serviceColour).darken(0.3);
return `${isSelected ? selectedSpanColor : serviceColour}`;
}, [isSelected, serviceColour, isDarkMode]);
return (
<SpanItemContainer
title={tooltipText}
onClick={handleClick}
onMouseEnter={(): void => {
handleHover(true);
}}
onMouseLeave={(): void => {
handleHover(false);
}}
topOffset={topOffset}
leftOffset={leftOffset}
width={width}
spanColor={spanColor}
selected={isSelected}
zIdx={isSelected ? 1 : 0}
/>
);
}
function TraceFlameGraph(props: {
treeData: ITraceTree;
traceMetaData: ITraceMetaData;
onSpanHover: SpanItemProps['onSpanHover'];
onSpanSelect: SpanItemProps['onSpanSelect'];
hoveredSpanId: string;
selectedSpanId: string;
missingSpanTree: boolean;
}): JSX.Element {
const { treeData, traceMetaData, onSpanHover, missingSpanTree } = props;
if (!treeData || treeData.id === 'empty' || !traceMetaData) {
return <div />;
}
const { onSpanSelect, hoveredSpanId, selectedSpanId } = props;
const { globalStart, spread, levels } = traceMetaData;
function RenderSpanRecursive({
level = 0,
spanData,
parentLeftOffset = 0,
onSpanHover,
onSpanSelect,
hoveredSpanId,
selectedSpanId,
}: {
spanData: ITraceTree;
level: number;
parentLeftOffset: number;
onSpanHover: SpanItemProps['onSpanHover'];
onSpanSelect: SpanItemProps['onSpanSelect'];
hoveredSpanId: string;
selectedSpanId: string;
}): JSX.Element {
if (!spanData) {
return <div />;
}
const leftOffset = ((spanData.startTime - globalStart) * 100) / spread;
const width = ((spanData.value / 1e6) * 100) / spread;
const toolTipText = `${spanData.name}`;
return (
<>
<SpanItem
topOffset={level * TOTAL_SPAN_HEIGHT}
leftOffset={leftOffset}
width={width}
spanData={spanData}
tooltipText={toolTipText}
onSpanHover={onSpanHover}
onSpanSelect={onSpanSelect}
hoveredSpanId={hoveredSpanId}
selectedSpanId={selectedSpanId}
/>
{spanData.children.map((childData) => (
<RenderSpanRecursive
level={level + 1}
spanData={childData}
key={childData.id}
parentLeftOffset={leftOffset + parentLeftOffset}
onSpanHover={onSpanHover}
onSpanSelect={onSpanSelect}
hoveredSpanId={hoveredSpanId}
selectedSpanId={selectedSpanId}
/>
))}
</>
);
}
return (
<TraceFlameGraphContainer height={TOTAL_SPAN_HEIGHT * levels}>
<RenderSpanRecursive
spanData={treeData}
onSpanHover={onSpanHover}
onSpanSelect={onSpanSelect}
hoveredSpanId={hoveredSpanId}
selectedSpanId={selectedSpanId}
level={missingSpanTree ? -1 : 0}
parentLeftOffset={0}
/>
</TraceFlameGraphContainer>
);
}
export default TraceFlameGraph;

View File

@@ -1,38 +0,0 @@
import styled from 'styled-components';
const SPAN_HEIGHT = 10;
const SPAN_V_PADDING = 1;
export const TOTAL_SPAN_HEIGHT = SPAN_HEIGHT + 2 * SPAN_V_PADDING;
/**
* An individual span for traces flame graph
*/
export const SpanItemContainer = styled.div<{
topOffset: number;
leftOffset: number;
width: number;
spanColor: string;
selected: boolean;
zIdx: number;
}>`
position: absolute;
top: ${(props): string | number => props.topOffset}px;
left: ${(props): string | number => props.leftOffset}%;
width: ${(props): string | number => props.width}%;
height: ${SPAN_HEIGHT}px;
margin: ${SPAN_V_PADDING}px 0;
background-color: ${({ spanColor }): string | number => spanColor};
border-radius: ${SPAN_HEIGHT / 2}px;
z-index: ${(props): string | number => props.zIdx};
`;
/**
* Container for spans, for traces flame graph.
*/
export const TraceFlameGraphContainer = styled.div<{
height: number;
}>`
position: relative;
width: 100%;
height: ${({ height }): string | number => height || 120}px;
`;

View File

@@ -1,4 +1,4 @@
import { Link } from 'react-router-dom';
import { generatePath, Link } from 'react-router-dom';
import type { TableColumnsType as ColumnsType } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { Typography } from '@signozhq/ui/typography';
@@ -7,8 +7,8 @@ import type { TracesTableRow } from 'container/TracesExplorer/TracesTable/getFie
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import ROUTES from 'constants/routes';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { formUrlParams } from 'container/TraceDetail/utils';
import { getMs } from 'utils/timeUtils';
import { formUrlParams } from 'utils/traceUtils';
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
@@ -53,7 +53,11 @@ export const getTraceLink = (record: Record<string, unknown>): string => {
const traceId = readId(record.traceID) || readId(record.trace_id);
const spanId = readId(record.spanID) || readId(record.span_id);
return `${ROUTES.TRACE}/${traceId}${formUrlParams({
if (!traceId) {
return '';
}
return `${generatePath(ROUTES.TRACE_DETAIL, { id: traceId })}${formUrlParams({
spanId,
levelUp: 0,
levelDown: 0,

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

View File

@@ -1,7 +1,7 @@
import RGL, { WidthProvider } from 'react-grid-layout';
import { Button as ButtonComponent, Card as CardComponent, Space } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { StyledCSS } from 'container/GantChart/Trace/styles';
import { StyledCSS } from 'types/styledComponents';
import styled, { css } from 'styled-components';
const ReactGridLayoutComponent = WidthProvider(RGL);

View File

@@ -11,7 +11,7 @@ import {
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import { PayloadProps } from 'types/api/metrics/getService';
import { Tags } from 'types/reducer/trace';
import { Tags } from 'hooks/useResourceAttribute/types';
export const useQueryService = ({
minTime,

View File

@@ -30,3 +30,27 @@ export interface IResourceAttributeProps {
optionsData: OptionsData;
handleEnvironmentChange: (environments: string[]) => void;
}
export type OperatorValues =
| 'NotIn'
| 'In'
| 'Equals'
| 'NotEquals'
| 'Contains'
| 'NotContains'
| 'GreaterThan'
| 'Exists'
| 'NotExists'
| 'LessThan'
| 'GreaterThanEquals'
| 'LessThanEquals'
| 'StartsWith'
| 'NotStartsWith';
export interface Tags {
Key: string;
Operator: OperatorValues;
StringValues: string[];
NumberValues: number[];
BoolValues: boolean[];
}

View File

@@ -17,7 +17,7 @@ import { decode } from 'js-base64';
import history from 'lib/history';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { OperatorValues, Tags } from 'types/reducer/trace';
import { OperatorValues, Tags } from 'hooks/useResourceAttribute/types';
import { v4 as uuid } from 'uuid';
import { whilelistedKeys } from './config';

View File

@@ -4,7 +4,7 @@ import { Badge } from '@signozhq/ui/badge';
import { Typography } from '@signozhq/ui/typography';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { getMs } from 'utils/timeUtils';
import type {
FormatTimezoneAdjustedTimestamp,
TimestampInput,

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