mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-13 00:10:45 +01:00
Compare commits
27 Commits
feat/semco
...
feat/initi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42242a93d0 | ||
|
|
16849967c5 | ||
|
|
c8e7685f06 | ||
|
|
a3caaaf7f2 | ||
|
|
a355996a5d | ||
|
|
eea11972a9 | ||
|
|
2616885d22 | ||
|
|
52dd57074e | ||
|
|
fe94b817db | ||
|
|
ea36032d96 | ||
|
|
6ecfa839f3 | ||
|
|
62d382b3cc | ||
|
|
cc07e2fa24 | ||
|
|
d7aa63f1bc | ||
|
|
c36b748370 | ||
|
|
89bf0f953a | ||
|
|
a850f548f8 | ||
|
|
5bf6fd9192 | ||
|
|
13f2ba7d34 | ||
|
|
848046de91 | ||
|
|
68e61af0be | ||
|
|
84780acee1 | ||
|
|
c70b2be4d5 | ||
|
|
cda2955b93 | ||
|
|
1a293652f7 | ||
|
|
0f3fb71067 | ||
|
|
b4f5b3eddf |
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -61,6 +61,7 @@ jobs:
|
||||
- querierauthz
|
||||
- role
|
||||
- rootuser
|
||||
- savedview
|
||||
- serviceaccount
|
||||
- spanmapper
|
||||
- querier_json_body
|
||||
|
||||
@@ -1499,6 +1499,7 @@ components:
|
||||
- computeengine
|
||||
- gke
|
||||
- cloudstorage
|
||||
- cloudsql_mysql
|
||||
type: string
|
||||
CloudintegrationtypesServiceMetadata:
|
||||
properties:
|
||||
@@ -7880,17 +7881,20 @@ components:
|
||||
type: string
|
||||
SavedviewtypesPostableSavedView:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
generateName:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
schemaVersion:
|
||||
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- source
|
||||
- data
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
SavedviewtypesSavedView:
|
||||
properties:
|
||||
@@ -7899,14 +7903,16 @@ components:
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
schemaVersion:
|
||||
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
@@ -7914,14 +7920,6 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
SavedviewtypesSavedViewData:
|
||||
properties:
|
||||
schemaVersion:
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
@@ -7936,7 +7934,10 @@ components:
|
||||
queries:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryEnvelope'
|
||||
minItems: 1
|
||||
type: array
|
||||
requestType:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5RequestType'
|
||||
selectedFields:
|
||||
items:
|
||||
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
|
||||
@@ -7944,10 +7945,13 @@ components:
|
||||
required:
|
||||
- displayName
|
||||
- panelType
|
||||
- requestType
|
||||
- queries
|
||||
- selectedFields
|
||||
- display
|
||||
type: object
|
||||
SavedviewtypesSchemaVersion:
|
||||
enum:
|
||||
- v2
|
||||
type: string
|
||||
SavedviewtypesSource:
|
||||
enum:
|
||||
- traces
|
||||
@@ -7957,13 +7961,16 @@ components:
|
||||
type: string
|
||||
SavedviewtypesUpdatableSavedView:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
schemaVersion:
|
||||
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- source
|
||||
- data
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
|
||||
properties:
|
||||
@@ -22776,6 +22783,12 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
|
||||
@@ -98,14 +98,6 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
Route: "",
|
||||
})
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.DotMetricsEnabled {
|
||||
featureSet[idx].Active = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ah.Respond(w, featureSet)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,15 +17,3 @@ func GetOrDefaultEnv(key string, fallback string) string {
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// constant functions that override env vars
|
||||
|
||||
const DotMetricsEnabled = "DOT_METRICS_ENABLED"
|
||||
|
||||
var IsDotMetricsEnabled = false
|
||||
|
||||
func init() {
|
||||
if GetOrDefaultEnv(DotMetricsEnabled, "true") == "true" {
|
||||
IsDotMetricsEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,7 +376,23 @@ function App(): JSX.Element {
|
||||
tracesSampleRate: 0, // Ref: https://github.com/SigNoz/platform-pod/issues/2393#issuecomment-4603658055
|
||||
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
|
||||
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
|
||||
beforeSend(event) {
|
||||
beforeSend(event, hint) {
|
||||
const error = hint?.originalException as
|
||||
| { name?: string; code?: string | number }
|
||||
| undefined;
|
||||
|
||||
// Ignore benign aborted/cancelled requests (axios + fetch).
|
||||
if (error?.code === 'ERR_CANCELED' || error?.code === 'ECONNABORTED') {
|
||||
return null;
|
||||
}
|
||||
if (error?.name === 'AbortError') {
|
||||
return null;
|
||||
}
|
||||
// Ignore benign Monaco cancellation errors (name 'Canceled').
|
||||
if (error?.name === 'Canceled') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Drop the event if its level is 'warning' or 'info'
|
||||
if (event.level === 'warning' || event.level === 'info') {
|
||||
return null;
|
||||
|
||||
@@ -2818,6 +2818,7 @@ export enum CloudintegrationtypesServiceIDDTO {
|
||||
computeengine = 'computeengine',
|
||||
gke = 'gke',
|
||||
cloudstorage = 'cloudstorage',
|
||||
cloudsql_mysql = 'cloudsql_mysql',
|
||||
}
|
||||
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
|
||||
/**
|
||||
@@ -8991,8 +8992,17 @@ export enum SavedviewtypesPanelTypeDTO {
|
||||
list = 'list',
|
||||
trace = 'trace',
|
||||
}
|
||||
export enum SavedviewtypesSchemaVersionDTO {
|
||||
v2 = 'v2',
|
||||
}
|
||||
export enum SavedviewtypesSourceDTO {
|
||||
traces = 'traces',
|
||||
logs = 'logs',
|
||||
metrics = 'metrics',
|
||||
meter = 'meter',
|
||||
}
|
||||
export interface SavedviewtypesSavedViewSpecDTO {
|
||||
display: SavedviewtypesDisplayDTO;
|
||||
display?: SavedviewtypesDisplayDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -9002,28 +9012,14 @@ export interface SavedviewtypesSavedViewSpecDTO {
|
||||
* @type array
|
||||
*/
|
||||
queries: Querybuildertypesv5QueryEnvelopeDTO[];
|
||||
requestType: Querybuildertypesv5RequestTypeDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
selectedFields: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
selectedFields?: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
}
|
||||
|
||||
export interface SavedviewtypesSavedViewDataDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
schemaVersion: string;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export enum SavedviewtypesSourceDTO {
|
||||
traces = 'traces',
|
||||
logs = 'logs',
|
||||
metrics = 'metrics',
|
||||
meter = 'meter',
|
||||
}
|
||||
export interface SavedviewtypesPostableSavedViewDTO {
|
||||
data: SavedviewtypesSavedViewDataDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -9032,7 +9028,9 @@ export interface SavedviewtypesPostableSavedViewDTO {
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO;
|
||||
source: SavedviewtypesSourceDTO;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export interface SavedviewtypesSavedViewDTO {
|
||||
@@ -9045,7 +9043,6 @@ export interface SavedviewtypesSavedViewDTO {
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
data?: SavedviewtypesSavedViewDataDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -9054,7 +9051,9 @@ export interface SavedviewtypesSavedViewDTO {
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO;
|
||||
source?: SavedviewtypesSourceDTO;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -9067,8 +9066,9 @@ export interface SavedviewtypesSavedViewDTO {
|
||||
}
|
||||
|
||||
export interface SavedviewtypesUpdatableSavedViewDTO {
|
||||
data: SavedviewtypesSavedViewDataDTO;
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO;
|
||||
source: SavedviewtypesSourceDTO;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {
|
||||
|
||||
@@ -24,19 +24,3 @@ export const Logout = async (): Promise<void> => {
|
||||
window.dispatchEvent(new CustomEvent('LOGOUT'));
|
||||
history.push(ROUTES.LOGIN);
|
||||
};
|
||||
|
||||
export const UnderscoreToDotMap: Record<string, string> = {
|
||||
k8s_cluster_name: 'k8s.cluster.name',
|
||||
k8s_cluster_uid: 'k8s.cluster.uid',
|
||||
k8s_namespace_name: 'k8s.namespace.name',
|
||||
k8s_node_name: 'k8s.node.name',
|
||||
k8s_node_uid: 'k8s.node.uid',
|
||||
k8s_pod_name: 'k8s.pod.name',
|
||||
k8s_pod_uid: 'k8s.pod.uid',
|
||||
k8s_deployment_name: 'k8s.deployment.name',
|
||||
k8s_daemonset_name: 'k8s.daemonset.name',
|
||||
k8s_statefulset_name: 'k8s.statefulset.name',
|
||||
k8s_cronjob_name: 'k8s.cronjob.name',
|
||||
k8s_job_name: 'k8s.job.name',
|
||||
k8s_persistentvolumeclaim_name: 'k8s.persistentvolumeclaim.name',
|
||||
};
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/user/resetPassword';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useResetPassword` hook (or `resetPassword` fetcher) from
|
||||
* `api/generated/services/users` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
const resetPassword = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>(`/resetPassword`, {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default resetPassword;
|
||||
@@ -1,31 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/user/setInvite';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useCreateInvite` hook (or `createInvite` fetcher) from
|
||||
* `api/generated/services/users` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
const sendInvite = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>(`/invite`, {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default sendInvite;
|
||||
@@ -5,10 +5,9 @@ import {
|
||||
useCreateResetPasswordToken,
|
||||
useDeleteUser,
|
||||
useGetResetPasswordToken,
|
||||
useGetRolesByUserID,
|
||||
useCreateUserRole,
|
||||
useDeleteUserRole,
|
||||
useGetUser,
|
||||
useRemoveUserRoleByUserIDAndRoleID,
|
||||
useSetRoleByUserID,
|
||||
useUpdateMyUserV2,
|
||||
useUpdateUser,
|
||||
} from 'api/generated/services/users';
|
||||
@@ -25,15 +24,14 @@ import EditMemberDrawer, { EditMemberDrawerProps } from '../EditMemberDrawer';
|
||||
jest.mock('api/generated/services/users', () => ({
|
||||
useDeleteUser: jest.fn(),
|
||||
useGetUser: jest.fn(),
|
||||
useGetRolesByUserID: jest.fn(),
|
||||
useRemoveUserRoleByUserIDAndRoleID: jest.fn(),
|
||||
useDeleteUserRole: jest.fn(),
|
||||
useUpdateUser: jest.fn(),
|
||||
useUpdateMyUserV2: jest.fn(),
|
||||
useSetRoleByUserID: jest.fn(),
|
||||
useCreateUserRole: jest.fn(),
|
||||
useGetResetPasswordToken: jest.fn(),
|
||||
useCreateResetPasswordToken: jest.fn(),
|
||||
getGetRolesByUserIDQueryKey: ({ id }: { id: string }): string[] => [
|
||||
`/api/v2/users/${id}/roles`,
|
||||
getGetUserQueryKey: ({ id }: { id: string }): string[] => [
|
||||
`/api/v2/users/${id}`,
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -194,11 +192,7 @@ describe('EditMemberDrawer', () => {
|
||||
isLoading: false,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
(useGetRolesByUserID as jest.Mock).mockReturnValue({
|
||||
data: { data: [managedRoles[0]] },
|
||||
isLoading: false,
|
||||
});
|
||||
(useRemoveUserRoleByUserIDAndRoleID as jest.Mock).mockReturnValue({
|
||||
(useDeleteUserRole as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockRemoveMutateAsync.mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
@@ -210,7 +204,7 @@ describe('EditMemberDrawer', () => {
|
||||
mutateAsync: jest.fn().mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
(useSetRoleByUserID as jest.Mock).mockReturnValue({
|
||||
(useCreateUserRole as jest.Mock).mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
@@ -312,12 +306,12 @@ describe('EditMemberDrawer', () => {
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('adding a new role calls setRole without removing existing ones', async () => {
|
||||
it('adding a new role creates a user role without removing existing ones', async () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockSet = jest.fn().mockResolvedValue({});
|
||||
|
||||
(useSetRoleByUserID as jest.Mock).mockReturnValue({
|
||||
(useCreateUserRole as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockSet,
|
||||
isLoading: false,
|
||||
});
|
||||
@@ -334,15 +328,14 @@ describe('EditMemberDrawer', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSet).toHaveBeenCalledWith({
|
||||
pathParams: { id: 'user-1' },
|
||||
data: { name: 'signoz-editor' },
|
||||
data: { userId: 'user-1', roleId: managedRoles[1].id },
|
||||
});
|
||||
expect(mockRemoveMutateAsync).not.toHaveBeenCalled();
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('deselecting a role calls removeRole with the role id', async () => {
|
||||
it('deselecting a role deletes the user role by its assignment id', async () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
@@ -361,7 +354,7 @@ describe('EditMemberDrawer', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRemoveMutateAsync).toHaveBeenCalledWith({
|
||||
pathParams: { id: 'user-1', roleId: managedRoles[0].id },
|
||||
pathParams: { id: 'ur-1' },
|
||||
});
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -10,6 +10,10 @@ jest.mock('providers/Timezone', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('providers/App/App', () => ({
|
||||
useAppContext: (): { featureFlags: [] } => ({ featureFlags: [] }),
|
||||
}));
|
||||
|
||||
const field = (name: string, type = ''): IField => ({
|
||||
name,
|
||||
type,
|
||||
|
||||
@@ -2,13 +2,15 @@ import type { ReactElement } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import {
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { getLogFieldValue } from 'lib/logs/flatLogData';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
@@ -26,6 +28,10 @@ export function useLogsTableColumns({
|
||||
fontSize,
|
||||
}: UseLogsTableColumnsProps): TableColumnDef<ILog>[] {
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
const { featureFlags } = useAppContext();
|
||||
const isBodyJsonEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
|
||||
?.active || false;
|
||||
|
||||
return useMemo<TableColumnDef<ILog>[]>(() => {
|
||||
const stateIndicatorCol: TableColumnDef<ILog> = {
|
||||
@@ -88,7 +94,8 @@ export function useLogsTableColumns({
|
||||
const makeUserFieldCol = (f: IField): TableColumnDef<ILog> => ({
|
||||
id: buildCompositeKey(f.name, f.type),
|
||||
header: f.name,
|
||||
accessorFn: (log): unknown => FlatLogData(log)[f.name],
|
||||
accessorFn: (log): unknown =>
|
||||
getLogFieldValue(log, f.name, isBodyJsonEnabled),
|
||||
enableRemove: true,
|
||||
width: { min: 192 },
|
||||
cell: ({ value }): ReactElement => (
|
||||
@@ -115,5 +122,5 @@ export function useLogsTableColumns({
|
||||
.filter((c): c is TableColumnDef<ILog> => c !== null);
|
||||
|
||||
return [stateIndicatorCol, ...fieldCols];
|
||||
}, [fields, fontSize, formatTimezoneAdjustedTimestamp]);
|
||||
}, [fields, fontSize, formatTimezoneAdjustedTimestamp, isBodyJsonEnabled]);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
dedupeOptionsByLabel,
|
||||
getFieldContextPrefix,
|
||||
getRecentOptions,
|
||||
isSupportedFunction,
|
||||
renderRecentDeleteButton,
|
||||
} from './utils';
|
||||
|
||||
@@ -183,15 +184,14 @@ function QuerySearch({
|
||||
isProgrammaticChangeRef.current = true;
|
||||
}
|
||||
|
||||
const changes = view.state.changes({
|
||||
from: 0,
|
||||
to: currentValue.length,
|
||||
insert: value,
|
||||
});
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: currentValue.length,
|
||||
insert: value,
|
||||
},
|
||||
selection: {
|
||||
anchor: value.length,
|
||||
},
|
||||
changes,
|
||||
selection: { anchor: changes.newLength },
|
||||
});
|
||||
},
|
||||
[],
|
||||
@@ -1276,11 +1276,13 @@ function QuerySearch({
|
||||
}
|
||||
|
||||
if (queryContext.isInFunction) {
|
||||
options = Object.values(QUERY_BUILDER_FUNCTIONS).map((option) => ({
|
||||
label: option,
|
||||
apply: `${option}()`,
|
||||
type: 'function',
|
||||
}));
|
||||
options = Object.values(QUERY_BUILDER_FUNCTIONS)
|
||||
.filter((option) => isSupportedFunction(option, dataSource))
|
||||
.map((option) => ({
|
||||
label: option,
|
||||
apply: `${option}()`,
|
||||
type: 'function',
|
||||
}));
|
||||
|
||||
// Add space after selection for functions
|
||||
const optionsWithSpace = addSpaceToOptions(options);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { QUERY_BUILDER_FUNCTIONS } from 'constants/antlrQueryConstants';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
dedupeOptionsByLabel,
|
||||
getFieldContextPrefix,
|
||||
getUserExpressionFromCombined,
|
||||
isSupportedFunction,
|
||||
} from '../utils';
|
||||
|
||||
describe('entityLogsExpression', () => {
|
||||
@@ -118,3 +122,19 @@ describe('dedupeOptionsByLabel', () => {
|
||||
expect(dedupeOptionsByLabel([])).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSupportedFunction', () => {
|
||||
const { HASANY, SEARCH } = QUERY_BUILDER_FUNCTIONS;
|
||||
|
||||
it('allows the has family on every signal', () => {
|
||||
[DataSource.LOGS, DataSource.TRACES, DataSource.METRICS].forEach((signal) => {
|
||||
expect(isSupportedFunction(HASANY, signal)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows search on logs only', () => {
|
||||
expect(isSupportedFunction(SEARCH, DataSource.LOGS)).toBe(true);
|
||||
expect(isSupportedFunction(SEARCH, DataSource.TRACES)).toBe(false);
|
||||
expect(isSupportedFunction(SEARCH, DataSource.METRICS)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { closeCompletion, startCompletion } from '@codemirror/autocomplete';
|
||||
import type { Completion } from '@codemirror/autocomplete';
|
||||
import type { EditorView } from '@uiw/react-codemirror';
|
||||
import { QUERY_BUILDER_FUNCTIONS } from 'constants/antlrQueryConstants';
|
||||
import dayjs from 'dayjs';
|
||||
import { normalizeFilterExpression } from 'lib/recentQueries/normalize';
|
||||
import * as recentQueriesStore from 'lib/recentQueries/recentQueriesStore';
|
||||
@@ -15,6 +16,15 @@ import {
|
||||
RECENTS_SECTION,
|
||||
} from './constants';
|
||||
|
||||
// search() lives in the logs condition builder only; traces and metrics reject it
|
||||
// as an unsupported operator. Every other function is implemented for all signals.
|
||||
export function isSupportedFunction(
|
||||
functionName: string,
|
||||
signal: SignalType,
|
||||
): boolean {
|
||||
return functionName !== QUERY_BUILDER_FUNCTIONS.SEARCH || signal === 'logs';
|
||||
}
|
||||
|
||||
export interface FieldContextPrefixMatch {
|
||||
context: string;
|
||||
remainder: string;
|
||||
|
||||
@@ -301,6 +301,66 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
dispatchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does not crash when the expression contains CRLF line breaks (issue #5869)', async () => {
|
||||
const dispatchSpy = jest.spyOn(EditorView.prototype, 'dispatch');
|
||||
const onChange = jest.fn() as jest.MockedFunction<(v: string) => void>;
|
||||
const initialExpression = "service.name = 'frontend'";
|
||||
// Filtering on a multi-line log value (CRLF) used to throw
|
||||
// "RangeError: Selection points outside of document".
|
||||
const crlfExpression = "body CONTAINS 'line1\r\nline2\r\nline3'";
|
||||
|
||||
const baseQueryData = {
|
||||
...initialQueriesMap.logs.builder.queryData[0],
|
||||
filter: { expression: initialExpression },
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<QuerySearch
|
||||
onChange={onChange}
|
||||
queryData={baseQueryData}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const editorContent = document.querySelector(
|
||||
CM_EDITOR_SELECTOR,
|
||||
) as HTMLElement;
|
||||
expect(editorContent.textContent || '').toBe(initialExpression);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
rerender(
|
||||
<QuerySearch
|
||||
onChange={onChange}
|
||||
queryData={{ ...baseQueryData, filter: { expression: crlfExpression } }}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>,
|
||||
);
|
||||
|
||||
// The programmatic replace dispatched without throwing, and the selection anchor
|
||||
// stayed within the CRLF-normalized document (the bug set it past the end).
|
||||
await waitFor(() => {
|
||||
const spec = dispatchSpy.mock.calls
|
||||
.map(
|
||||
(call) =>
|
||||
call[0] as {
|
||||
selection?: { anchor?: number };
|
||||
changes?: { newLength?: number };
|
||||
},
|
||||
)
|
||||
.find((s) => s?.selection?.anchor != null && s?.changes?.newLength != null);
|
||||
expect(spec).toBeDefined();
|
||||
expect(spec?.selection?.anchor).toBeLessThanOrEqual(
|
||||
spec?.changes?.newLength as number,
|
||||
);
|
||||
});
|
||||
|
||||
dispatchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('fetches key suggestions for metrics even without aggregateAttribute.key when showFilterSuggestionsWithoutMetric is true', async () => {
|
||||
const mockedGetKeys = getKeySuggestions as jest.MockedFunction<
|
||||
typeof getKeySuggestions
|
||||
|
||||
@@ -41,6 +41,7 @@ export const QUERY_BUILDER_FUNCTIONS = {
|
||||
HASANY: 'hasAny',
|
||||
HASALL: 'hasAll',
|
||||
HASTOKEN: 'hasToken',
|
||||
SEARCH: 'search',
|
||||
};
|
||||
|
||||
export function negateOperator(operatorOrFunction: string): string {
|
||||
|
||||
@@ -7,7 +7,6 @@ export enum FeatureKeys {
|
||||
GATEWAY = 'gateway',
|
||||
PREMIUM_SUPPORT = 'premium_support',
|
||||
ANOMALY_DETECTION = 'anomaly_detection',
|
||||
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
|
||||
USE_JSON_BODY = 'use_json_body',
|
||||
ENABLE_AI_OBSERVABILITY = 'enable_ai_observability',
|
||||
ENABLE_METRICS_REDUCTION = 'enable_metrics_reduction',
|
||||
|
||||
@@ -11,6 +11,8 @@ export enum LOCALSTORAGE {
|
||||
TRACES_LIST_OPTIONS = 'TRACES_LIST_OPTIONS',
|
||||
GRAPH_VISIBILITY_STATES = 'GRAPH_VISIBILITY_STATES',
|
||||
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
|
||||
AI_OBSERVABILITY_LIST_COLUMNS = 'AI_OBSERVABILITY_LIST_COLUMNS',
|
||||
AI_OBSERVABILITY_TRACE_VIEW_COLUMNS = 'AI_OBSERVABILITY_TRACE_VIEW_COLUMNS',
|
||||
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMN_SIZING = 'LOGS_LIST_COLUMN_SIZING',
|
||||
LOG_DETAILS_V2 = 'LOG_DETAILS_V2',
|
||||
|
||||
@@ -437,6 +437,17 @@ describe('Create Alert Channel', () => {
|
||||
render(<CreateAlertChannels preType={ChannelType.GoogleChat} />);
|
||||
});
|
||||
|
||||
// paste instead of type: a per-keystroke re-render of the whole form
|
||||
// pushes these tests past the 5s jest timeout on slower CI runners
|
||||
async function fillField(
|
||||
user: ReturnType<typeof userEvent.setup>,
|
||||
testId: string,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
await user.click(screen.getByTestId(testId));
|
||||
await user.paste(value);
|
||||
}
|
||||
|
||||
it('Should check if the selected item in the type dropdown has text "Google Chat"', () => {
|
||||
expect(screen.getByText('Google Chat')).toBeInTheDocument();
|
||||
});
|
||||
@@ -463,14 +474,8 @@ describe('Create Alert Channel', () => {
|
||||
it('Should check if saving with a webhook url outside chat.googleapis.com displays error notification', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.type(
|
||||
screen.getByTestId('channel-name-textbox'),
|
||||
'gchat-channel',
|
||||
);
|
||||
await user.type(
|
||||
screen.getByTestId('webhook-url-textbox'),
|
||||
'https://example.com/webhook',
|
||||
);
|
||||
await fillField(user, 'channel-name-textbox', 'gchat-channel');
|
||||
await fillField(user, 'webhook-url-textbox', 'https://example.com/webhook');
|
||||
|
||||
await user.click(screen.getByTestId('save-channel-button'));
|
||||
|
||||
@@ -496,11 +501,8 @@ describe('Create Alert Channel', () => {
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.type(
|
||||
screen.getByTestId('channel-name-textbox'),
|
||||
'gchat-channel',
|
||||
);
|
||||
await user.type(screen.getByTestId('webhook-url-textbox'), validWebhookUrl);
|
||||
await fillField(user, 'channel-name-textbox', 'gchat-channel');
|
||||
await fillField(user, 'webhook-url-textbox', validWebhookUrl);
|
||||
|
||||
await user.click(screen.getByTestId('save-channel-button'));
|
||||
|
||||
|
||||
@@ -37,8 +37,6 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Exception, PayloadProps } from 'types/api/errors/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { FeatureKeys } from '../../constants/features';
|
||||
import { useAppContext } from '../../providers/App/App';
|
||||
import { FilterDropdownExtendsProps } from './types';
|
||||
import {
|
||||
extractFilterValues,
|
||||
@@ -418,11 +416,6 @@ function AllErrors(): JSX.Element {
|
||||
},
|
||||
];
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const onChangeHandler: TableProps<Exception>['onChange'] = useCallback(
|
||||
(
|
||||
paginations: TablePaginationConfig,
|
||||
@@ -458,7 +451,7 @@ function AllErrors(): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (!isUndefined(errorCountResponse.data?.payload)) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(),
|
||||
)?.tagValue;
|
||||
|
||||
logEvent('Exception: List page visited', {
|
||||
|
||||
@@ -130,6 +130,28 @@ describe('Footer utils', () => {
|
||||
};
|
||||
expect(validateCreateAlertState(currentArgs)).toBeNull();
|
||||
});
|
||||
|
||||
it('when threshold channels are null', () => {
|
||||
const currentArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
...args,
|
||||
basicAlertState: {
|
||||
...args.basicAlertState,
|
||||
name: 'test name',
|
||||
},
|
||||
thresholdState: {
|
||||
...args.thresholdState,
|
||||
thresholds: [
|
||||
{
|
||||
...args.thresholdState.thresholds[0],
|
||||
channels: null as unknown as string[],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(validateCreateAlertState(currentArgs)).toBe(
|
||||
'Please select at least one channel for each threshold or enable routing policies',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNotificationSettingsProps', () => {
|
||||
|
||||
@@ -44,7 +44,8 @@ export function validateCreateAlertState(
|
||||
if (!threshold.label) {
|
||||
return 'Please enter a label for each threshold';
|
||||
}
|
||||
if (!notificationSettings.routingPolicies && !threshold.channels.length) {
|
||||
// this runs during render, so a throw here takes down the whole page
|
||||
if (!notificationSettings.routingPolicies && !threshold.channels?.length) {
|
||||
return 'Please select at least one channel for each threshold or enable routing policies';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +316,34 @@ describe('CreateAlertV2 utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThresholdStateFromAlertDef null channels', () => {
|
||||
it('falls back to an empty array so downstream consumers never see null', () => {
|
||||
const def: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
condition: {
|
||||
...defaultPostableAlertRuleV2.condition,
|
||||
thresholds: {
|
||||
kind: 'basic',
|
||||
spec: [
|
||||
{
|
||||
name: 'critical',
|
||||
target: 1,
|
||||
targetUnit: UniversalYAxisUnit.MINUTES,
|
||||
channels: null as unknown as string[],
|
||||
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
op: AlertThresholdOperator.IS_ABOVE,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
getThresholdStateFromAlertDef(def).thresholds[0].channels,
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeOperator', () => {
|
||||
it.each([
|
||||
['1', AlertThresholdOperator.IS_ABOVE],
|
||||
|
||||
@@ -258,7 +258,9 @@ export function getThresholdStateFromAlertDef(
|
||||
recoveryThresholdValue: null,
|
||||
unit: threshold.targetUnit,
|
||||
color: getColorForThreshold(threshold.name),
|
||||
channels: threshold.channels,
|
||||
// rules created outside the UI can come back with a null channels
|
||||
// field; drop the guard once the API enforces the schema
|
||||
channels: threshold.channels ?? [],
|
||||
})) || [],
|
||||
selectedQuery: alertDef.condition.selectedQueryName || '',
|
||||
operator:
|
||||
|
||||
@@ -35,7 +35,6 @@ import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { DOCS_LINKS } from '../constants';
|
||||
import { columns, TIME_PICKER_OPTIONS } from './constants';
|
||||
|
||||
@@ -212,19 +211,13 @@ function ServiceMetrics({
|
||||
|
||||
const topLevelOperations = useMemo(() => Object.entries(data || {}), [data]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const queryRangeRequestData = useMemo(
|
||||
() =>
|
||||
getQueryRangeRequestData({
|
||||
topLevelOperations,
|
||||
globalSelectedInterval,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
[globalSelectedInterval, topLevelOperations, dotMetricsEnabled],
|
||||
[globalSelectedInterval, topLevelOperations],
|
||||
);
|
||||
|
||||
const dataQueries = useGetQueriesRange(
|
||||
|
||||
@@ -562,13 +562,9 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY})`,
|
||||
op: '=',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
having: {
|
||||
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY}) = 1`,
|
||||
},
|
||||
legend: `{{${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME}}}`,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -648,13 +644,9 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY})`,
|
||||
op: '=',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: {
|
||||
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY}) = 0`,
|
||||
},
|
||||
legend: `{{${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME}}}`,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
|
||||
@@ -22,6 +22,7 @@ import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants'
|
||||
import LoadingContainer from 'container/InfraMonitoringK8sV2/LoadingContainer';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
import { saveRecentQueryByExpression } from 'lib/recentQueries/saveRecentQuery';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
@@ -116,6 +117,7 @@ function EntityEventsContent({
|
||||
: newUserExpression || '',
|
||||
);
|
||||
if (validation.isValid) {
|
||||
saveRecentQueryByExpression(DataSource.LOGS, newUserExpression);
|
||||
querySearchOnRun(newUserExpression || '');
|
||||
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog';
|
||||
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
|
||||
import useScrollToLog from 'hooks/logs/useScrollToLog';
|
||||
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
||||
import { saveRecentQueryByExpression } from 'lib/recentQueries/saveRecentQuery';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
@@ -132,6 +133,7 @@ function EntityLogsContent({
|
||||
);
|
||||
|
||||
if (validation.isValid) {
|
||||
saveRecentQueryByExpression(DataSource.LOGS, newUserExpression);
|
||||
querySearchOnRun(newUserExpression);
|
||||
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
|
||||
@@ -121,12 +121,6 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
|
||||
plan_version: 'test-plan-version',
|
||||
},
|
||||
},
|
||||
featureFlags: [
|
||||
{
|
||||
name: 'DOT_METRICS_ENABLED',
|
||||
active: false,
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
|
||||
const mockEntity = {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants'
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
|
||||
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
|
||||
import { saveRecentQueryByExpression } from 'lib/recentQueries/saveRecentQuery';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
@@ -98,6 +99,7 @@ function EntityTracesContent({
|
||||
: newUserExpression || '',
|
||||
);
|
||||
if (validation.isValid) {
|
||||
saveRecentQueryByExpression(DataSource.TRACES, newUserExpression);
|
||||
querySearchOnRun(newUserExpression || '');
|
||||
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
|
||||
@@ -1208,13 +1208,9 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: {
|
||||
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED}) > 0`,
|
||||
},
|
||||
legend: 'desired',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -1261,13 +1257,9 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: {
|
||||
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_AVAILABLE}) > 0`,
|
||||
},
|
||||
legend: 'available',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.3rem;
|
||||
margin: var(--spacing-4) 0;
|
||||
}
|
||||
|
||||
.optionsTrigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
cursor: pointer;
|
||||
|
||||
// Was a bare div in the traces explorer; a <button> for keyboard access
|
||||
// needs its chrome reset back to the original look.
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Settings } from '@signozhq/icons';
|
||||
import FieldsSelector from 'components/FieldsSelector';
|
||||
import Controls, { ControlsProps } from 'container/Controls';
|
||||
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||
import useQueryPagination from 'hooks/queryPagination/useQueryPagination';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import styles from './Controls.module.scss';
|
||||
|
||||
function ExplorerControls({
|
||||
isLoading,
|
||||
totalCount,
|
||||
perPageOptions,
|
||||
config,
|
||||
showSizeChanger = true,
|
||||
}: ExplorerControlsProps): JSX.Element | null {
|
||||
const { t } = useTranslation(['trace']);
|
||||
const [isFieldsSelectorOpen, setIsFieldsSelectorOpen] = useState(false);
|
||||
|
||||
const {
|
||||
pagination,
|
||||
handleCountItemsPerPageChange,
|
||||
handleNavigateNext,
|
||||
handleNavigatePrevious,
|
||||
} = useQueryPagination(totalCount, perPageOptions);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{config?.fieldsSelector && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.optionsTrigger}
|
||||
onClick={(): void => setIsFieldsSelectorOpen(true)}
|
||||
data-testid="explorer-controls-options"
|
||||
>
|
||||
{t('options_menu.options')}
|
||||
<Settings size="md" />
|
||||
</button>
|
||||
<FieldsSelector
|
||||
isOpen={isFieldsSelectorOpen}
|
||||
title="Edit columns"
|
||||
fields={config.fieldsSelector.value}
|
||||
onFieldsChange={config.fieldsSelector.onFieldsChange}
|
||||
onClose={(): void => setIsFieldsSelectorOpen(false)}
|
||||
signal={DataSource.TRACES}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Controls
|
||||
isLoading={isLoading}
|
||||
totalCount={totalCount}
|
||||
offset={pagination.offset}
|
||||
countPerPage={pagination.limit}
|
||||
perPageOptions={perPageOptions}
|
||||
handleCountItemsPerPageChange={handleCountItemsPerPageChange}
|
||||
handleNavigateNext={handleNavigateNext}
|
||||
handleNavigatePrevious={handleNavigatePrevious}
|
||||
showSizeChanger={showSizeChanger}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ExplorerControlsProps = Pick<
|
||||
ControlsProps,
|
||||
'isLoading' | 'totalCount' | 'perPageOptions'
|
||||
> & {
|
||||
config?: OptionsMenuConfig | null;
|
||||
showSizeChanger?: boolean;
|
||||
};
|
||||
|
||||
ExplorerControls.defaultProps = {
|
||||
config: null,
|
||||
showSizeChanger: true,
|
||||
};
|
||||
|
||||
export default memo(ExplorerControls);
|
||||
@@ -1,11 +1,56 @@
|
||||
.explorer {
|
||||
.explorerPage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-2) var(--spacing-0);
|
||||
|
||||
// Meant to fix the query builder colors
|
||||
--input-background: var(--l2-background);
|
||||
--input-hover-background: var(--l2-background);
|
||||
--input-focus-background: var(--l2-background);
|
||||
--input-border-color: var(--l2-border);
|
||||
--input-hover-border-color: var(--internal-ant-border-color-hover);
|
||||
--input-focus-border-color: var(--internal-ant-border-color-hover);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
.filter {
|
||||
width: 260px;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
border: 1px solid var(--l1-border);
|
||||
border-right: 0px;
|
||||
background-color: var(--l1-background);
|
||||
|
||||
> :global(.ant-card-body) {
|
||||
padding: 0;
|
||||
width: 258px;
|
||||
}
|
||||
}
|
||||
|
||||
.explorer {
|
||||
width: 100%;
|
||||
background: var(--l1-background);
|
||||
border-color: var(--l1-border);
|
||||
|
||||
> :global(.ant-card-body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.isFiltersExpanded {
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
|
||||
.views {
|
||||
padding: var(--spacing-4);
|
||||
// Room the traces explorer reserves for its floating options bar, which this
|
||||
// explorer doesn't render yet. No spacing token for 60px.
|
||||
padding-bottom: 60px;
|
||||
margin-bottom: var(--spacing-12);
|
||||
|
||||
:global(.ant-tabs-tabpane) {
|
||||
padding: 0 var(--spacing-4);
|
||||
}
|
||||
}
|
||||
|
||||
.view {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,253 @@
|
||||
import styles from './Explorer.module.scss';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { QueryKey, useQueryClient } from 'react-query';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Card } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import Toolbar from 'container/Toolbar/Toolbar';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import {
|
||||
ICurrentQueryData,
|
||||
useHandleExplorerTabChange,
|
||||
} from 'hooks/useHandleExplorerTabChange';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { Warning } from 'types/api';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import {
|
||||
explorerViewToPanelType,
|
||||
getExplorerViewFromUrl,
|
||||
} from 'utils/explorerUtils';
|
||||
|
||||
// Shell for the AI Observability Explorer tab. Owns the
|
||||
// /ai-observability/explorer route and is intentionally empty for now: the
|
||||
// query builder + results surface land in a follow-up.
|
||||
import { defaultSelectedColumns, TOOLBAR_VIEWS } from './constants';
|
||||
import styles from './Explorer.module.scss';
|
||||
import ListView from './ListView/ListView';
|
||||
import QuerySection from './QuerySection/QuerySection';
|
||||
import TableView from './TableView/TableView';
|
||||
import TimeSeriesView from './TimeSeriesView/TimeSeriesView';
|
||||
import TracesView from './TracesView/TracesView';
|
||||
|
||||
// AI Observability Explorer tab. Owns the /ai-observability/explorer route.
|
||||
// Forked from the Traces Explorer (GenAI spans are trace data) and diverges
|
||||
// from it as the GenAI query surface lands, so the two are deliberately not
|
||||
// sharing a component.
|
||||
function Explorer(): JSX.Element {
|
||||
const {
|
||||
updateAllQueriesOperators,
|
||||
handleRunQuery,
|
||||
stagedQuery,
|
||||
handleSetConfig,
|
||||
} = useQueryBuilder();
|
||||
|
||||
// Return value unused: this seeds the URL `options` param on the views that
|
||||
// don't mount ListView (which runs its own useOptionsMenu).
|
||||
useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.TRACES_LIST_OPTIONS,
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'noop',
|
||||
initialOptions: {
|
||||
selectColumns: defaultSelectedColumns,
|
||||
},
|
||||
});
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const queryClient = useQueryClient();
|
||||
const listQueryKeyRef = useRef<QueryKey>();
|
||||
|
||||
// Get panel type from URL
|
||||
const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
const [isLoadingQueries, setIsLoadingQueries] = useState<boolean>(false);
|
||||
const [isCancelled, setIsCancelled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadingQueries) {
|
||||
setIsCancelled(false);
|
||||
}
|
||||
}, [isLoadingQueries]);
|
||||
|
||||
const handleCancelQuery = useCallback(() => {
|
||||
if (listQueryKeyRef.current) {
|
||||
void queryClient.cancelQueries(listQueryKeyRef.current);
|
||||
}
|
||||
setIsCancelled(true);
|
||||
// Reset loading state — the active view unmounts when cancelled, so no
|
||||
// child will call setIsLoadingQueries(false) otherwise.
|
||||
setIsLoadingQueries(false);
|
||||
}, [queryClient]);
|
||||
|
||||
const [selectedView, setSelectedView] = useState<ExplorerViews>(() =>
|
||||
getExplorerViewFromUrl(searchParams, panelTypesFromUrl),
|
||||
);
|
||||
|
||||
const [warning, setWarning] = useState<Warning | undefined>();
|
||||
const [isOpen, setOpen] = useState<boolean>(true);
|
||||
|
||||
const defaultQuery = useMemo(
|
||||
(): Query =>
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.traces,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.TRACES,
|
||||
),
|
||||
[updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
|
||||
const handleChangeSelectedView = useCallback(
|
||||
(view: ExplorerViews, querySearchParameters?: ICurrentQueryData): void => {
|
||||
handleSetConfig(explorerViewToPanelType[view], DataSource.TRACES);
|
||||
|
||||
setSelectedView(view);
|
||||
|
||||
handleExplorerTabChange(
|
||||
explorerViewToPanelType[view],
|
||||
querySearchParameters,
|
||||
);
|
||||
},
|
||||
[handleExplorerTabChange, handleSetConfig],
|
||||
);
|
||||
|
||||
useShareBuilderUrl({ defaultValue: defaultQuery });
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
void logEvent('AI Observability Explorer: Page visited', {});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const isFilterApplied = useMemo(() => {
|
||||
// if any of the non-disabled queries has filters applied, return true
|
||||
const result = stagedQuery?.builder?.queryData?.filter(
|
||||
(item) => !isEmpty(item.filters?.items) && !item.disabled,
|
||||
);
|
||||
return !!result?.length;
|
||||
}, [stagedQuery]);
|
||||
|
||||
return (
|
||||
<div className={styles.explorer} data-testid="llm-observability-explorer">
|
||||
<div className={styles.placeholder}>Explorer coming soon.</div>
|
||||
</div>
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div
|
||||
className={styles.explorerPage}
|
||||
data-testid="llm-observability-explorer"
|
||||
>
|
||||
<Card className={styles.filter} hidden={!isOpen}>
|
||||
<QuickFilters
|
||||
className="qf-traces-explorer"
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
signal={SignalType.TRACES}
|
||||
handleFilterVisibilityChange={(): void => {
|
||||
setOpen(!isOpen);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<div
|
||||
className={cx(styles.explorer, {
|
||||
[styles.isFiltersExpanded]: isOpen,
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<Toolbar
|
||||
showAutoRefresh
|
||||
leftActions={
|
||||
<LeftToolbarActions
|
||||
showFilter={isOpen}
|
||||
handleFilterVisibilityChange={(): void => setOpen(!isOpen)}
|
||||
items={TOOLBAR_VIEWS}
|
||||
selectedView={selectedView}
|
||||
onChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
}
|
||||
warningElement={
|
||||
!isEmpty(warning) ? <WarningPopover warningData={warning} /> : <div />
|
||||
}
|
||||
rightActions={
|
||||
<RightToolbarActions
|
||||
onStageRunQuery={(): void => {
|
||||
setIsCancelled(false);
|
||||
handleRunQuery();
|
||||
}}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>
|
||||
<div className="query-section-container">
|
||||
<QuerySection />
|
||||
</div>
|
||||
</ExplorerCard>
|
||||
|
||||
<div className={styles.views}>
|
||||
{isCancelled && (
|
||||
<QueryCancelledPlaceholder subText='Click "Run Query" to load traces.' />
|
||||
)}
|
||||
|
||||
{!isCancelled && selectedView === ExplorerViews.LIST && (
|
||||
<div className={styles.view}>
|
||||
<ListView
|
||||
isFilterApplied={isFilterApplied}
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCancelled && selectedView === ExplorerViews.TRACE && (
|
||||
<div className={styles.view}>
|
||||
<TracesView
|
||||
isFilterApplied={isFilterApplied}
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCancelled && selectedView === ExplorerViews.TIMESERIES && (
|
||||
<div className={styles.view}>
|
||||
<TimeSeriesView
|
||||
dataSource={DataSource.TRACES}
|
||||
isFilterApplied={isFilterApplied}
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCancelled && selectedView === ExplorerViews.TABLE && (
|
||||
<div className={styles.view}>
|
||||
<TableView
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--typography-color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.orderByContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.orderByLabel {
|
||||
color: var(--muted-foreground);
|
||||
// Between --periscope-font-size-small (11px) and -base (13px), so literal.
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px; /* 133.333% */
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
||||
import NoLogs from 'container/NoLogs/NoLogs';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { getDefaultPaginationConfig } from 'hooks/queryPagination/utils';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { ArrowUp10, Minus } from '@signozhq/icons';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
|
||||
import { defaultSelectedColumns, PER_PAGE_OPTIONS } from '../constants';
|
||||
import ExplorerControls from '../Controls/Controls';
|
||||
import { getListViewQuery } from '../explorerUtils';
|
||||
import { TracesLoading } from '../TraceLoading/TraceLoading';
|
||||
import styles from './ListView.module.scss';
|
||||
import { useListTableColumns } from './useListTableColumns';
|
||||
import { TraceListRow } from '../tableUtils';
|
||||
import { getTraceLink, getTraceRowKey, transformDataWithDate } from './utils';
|
||||
|
||||
interface ListViewProps {
|
||||
isFilterApplied: boolean;
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<QueryKey | undefined>;
|
||||
}
|
||||
|
||||
function ListView({
|
||||
isFilterApplied,
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: ListViewProps): JSX.Element {
|
||||
const { stagedQuery, panelType: panelTypeFromQueryBuilder } =
|
||||
useQueryBuilder();
|
||||
|
||||
const panelType = panelTypeFromQueryBuilder || PANEL_TYPES.LIST;
|
||||
|
||||
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
loading: timeRangeUpdateLoading,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
|
||||
const { options, config } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.TRACES_LIST_OPTIONS,
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'count',
|
||||
initialOptions: {
|
||||
selectColumns: defaultSelectedColumns,
|
||||
},
|
||||
});
|
||||
|
||||
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||
QueryParams.pagination,
|
||||
);
|
||||
const paginationConfig =
|
||||
paginationQueryData ?? getDefaultPaginationConfig(PER_PAGE_OPTIONS);
|
||||
|
||||
const requestQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueriesMap.traces, orderBy),
|
||||
[stagedQuery, orderBy],
|
||||
);
|
||||
|
||||
// TEMP — remove after traces moves to TanStack table.
|
||||
// - Drag updates selectColumns; raw queryKey would churn on reorder.
|
||||
// - Trace API fetches only listed columns → add/remove must refetch.
|
||||
// - Sorted-name signature: stable on reorder, changes on add/remove.
|
||||
const selectColumnsSignature = useMemo(
|
||||
() =>
|
||||
(options?.selectColumns ?? [])
|
||||
.map((c) => c.name)
|
||||
.sort()
|
||||
.join(','),
|
||||
[options?.selectColumns],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationConfig,
|
||||
selectColumnsSignature,
|
||||
orderBy,
|
||||
],
|
||||
[
|
||||
stagedQuery,
|
||||
panelType,
|
||||
globalSelectedTime,
|
||||
paginationConfig,
|
||||
selectColumnsSignature,
|
||||
maxTime,
|
||||
minTime,
|
||||
orderBy,
|
||||
],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
|
||||
const { data, isFetching, isLoading, isError, error } = useGetQueryRange(
|
||||
{
|
||||
query: requestQuery,
|
||||
graphType: panelType,
|
||||
selectedTime: 'GLOBAL_TIME' as const,
|
||||
globalSelectedInterval: globalSelectedTime as CustomTimeType,
|
||||
params: {
|
||||
dataSource: 'traces',
|
||||
},
|
||||
tableParams: {
|
||||
pagination: paginationConfig,
|
||||
selectColumns: options?.selectColumns,
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey,
|
||||
enabled:
|
||||
// don't make api call while the time range state in redux is loading
|
||||
!timeRangeUpdateLoading &&
|
||||
!!stagedQuery &&
|
||||
panelType === PANEL_TYPES.LIST &&
|
||||
!!options?.selectColumns?.length,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload) {
|
||||
setWarning(data?.warning);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data?.payload, data?.warning]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || isFetching) {
|
||||
setIsLoadingQueries(true);
|
||||
} else {
|
||||
setIsLoadingQueries(false);
|
||||
}
|
||||
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||
|
||||
const dataLength =
|
||||
data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
|
||||
const totalCount = useMemo(() => dataLength || 0, [dataLength]);
|
||||
|
||||
const queryTableDataResult = data?.payload?.data?.newResult?.data?.result;
|
||||
const queryTableData = useMemo(
|
||||
() => queryTableDataResult || [],
|
||||
[queryTableDataResult],
|
||||
);
|
||||
|
||||
const columns = useListTableColumns(options?.selectColumns || []);
|
||||
|
||||
const transformedQueryTableData = useMemo(
|
||||
() => transformDataWithDate(queryTableData) || [],
|
||||
[queryTableData],
|
||||
);
|
||||
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const handleColumnOrderChange = useCallback(
|
||||
(reordered: TableColumnDef<TraceListRow>[]): void => {
|
||||
// Column ids are composite (fieldContext.name) — disambiguates same-name fields.
|
||||
config?.addColumn?.onReorder(reordered.map((column) => column.id));
|
||||
},
|
||||
[config],
|
||||
);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(row: TraceListRow): void => {
|
||||
safeNavigate(getTraceLink(row));
|
||||
},
|
||||
[safeNavigate],
|
||||
);
|
||||
|
||||
const handleRowClickNewTab = useCallback((row: TraceListRow): void => {
|
||||
window.open(getAbsoluteUrl(getTraceLink(row)), '_blank');
|
||||
}, []);
|
||||
|
||||
const handleOrderChange = useCallback((value: string) => {
|
||||
setOrderBy(value);
|
||||
}, []);
|
||||
|
||||
const isDataAbsent =
|
||||
!isLoading &&
|
||||
!isFetching &&
|
||||
!isError &&
|
||||
transformedQueryTableData.length === 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isLoading &&
|
||||
!isFetching &&
|
||||
!isError &&
|
||||
transformedQueryTableData.length !== 0
|
||||
) {
|
||||
void logEvent('AI Observability Explorer: Data present', {
|
||||
panelType,
|
||||
});
|
||||
}
|
||||
}, [isLoading, isFetching, isError, transformedQueryTableData, panelType]);
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.controls}>
|
||||
<div className={styles.orderByContainer}>
|
||||
<div className={styles.orderByLabel}>
|
||||
Order by <Minus size={14} /> <ArrowUp10 size={14} />
|
||||
</div>
|
||||
|
||||
<ListViewOrderBy
|
||||
value={orderBy}
|
||||
onChange={handleOrderChange}
|
||||
dataSource={DataSource.TRACES}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DownloadOptionsMenu
|
||||
dataSource={DataSource.TRACES}
|
||||
selectedColumns={options?.selectColumns}
|
||||
/>
|
||||
|
||||
<ExplorerControls
|
||||
isLoading={isFetching}
|
||||
totalCount={totalCount}
|
||||
config={config}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isError && error && <ErrorInPlace error={error as APIError} />}
|
||||
|
||||
{(isLoading || (isFetching && transformedQueryTableData.length === 0)) && (
|
||||
<TracesLoading />
|
||||
)}
|
||||
|
||||
{isDataAbsent && !isFilterApplied && (
|
||||
<NoLogs dataSource={DataSource.TRACES} />
|
||||
)}
|
||||
|
||||
{isDataAbsent && isFilterApplied && (
|
||||
<EmptyLogsSearch dataSource={DataSource.TRACES} panelType="LIST" />
|
||||
)}
|
||||
|
||||
{!isError && transformedQueryTableData.length !== 0 && (
|
||||
<TanStackTable<TraceListRow>
|
||||
data={transformedQueryTableData}
|
||||
columns={columns}
|
||||
columnStorageKey={LOCALSTORAGE.AI_OBSERVABILITY_LIST_COLUMNS}
|
||||
respectColumnOrder={false}
|
||||
onColumnOrderChange={handleColumnOrderChange}
|
||||
onColumnRemove={config?.addColumn?.onRemove}
|
||||
isLoading={isFetching}
|
||||
getRowKey={getTraceRowKey}
|
||||
onRowClick={handleRowClick}
|
||||
onRowClickNewTab={handleRowClickNewTab}
|
||||
// Rows are already page-sized by the query; virtualizing would need a
|
||||
// fixed-height container this page doesn't provide.
|
||||
disableVirtualScroll
|
||||
testId="ai-observability-list-view-table"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ListView.defaultProps = {
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default memo(ListView);
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
import { formatCellValue, TraceListRow } from '../tableUtils';
|
||||
|
||||
/** Older callers passed `{ key, type }` where v5 uses `{ name, fieldContext }`. */
|
||||
interface LegacyFieldKey {
|
||||
key?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
const BADGE_FIELDS = new Set([
|
||||
'httpMethod',
|
||||
'responseStatusCode',
|
||||
'response_status_code',
|
||||
'http_method',
|
||||
]);
|
||||
|
||||
const DURATION_FIELDS = new Set(['durationNano', 'duration_nano']);
|
||||
|
||||
const TIMESTAMP_COLUMN_ID = 'date';
|
||||
|
||||
/**
|
||||
* Column definitions for the list view's TanStack table.
|
||||
*
|
||||
* Cell renderers must tolerate missing values: the table feeds skeleton
|
||||
* placeholder rows through them before the first response lands.
|
||||
*/
|
||||
export function useListTableColumns(
|
||||
selectedColumns: TelemetryFieldKey[],
|
||||
): TableColumnDef<TraceListRow>[] {
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
return useMemo<TableColumnDef<TraceListRow>[]>(() => {
|
||||
const timestampColumn: TableColumnDef<TraceListRow> = {
|
||||
id: TIMESTAMP_COLUMN_ID,
|
||||
header: 'Timestamp',
|
||||
accessorFn: (row): unknown => row?.date,
|
||||
canBeHidden: false,
|
||||
enableRemove: false,
|
||||
width: { default: 145, min: 145 },
|
||||
cell: ({ value }): ReactElement => {
|
||||
const timestamp = value as string | number | undefined;
|
||||
if (timestamp === undefined || timestamp === null) {
|
||||
return <TanStackTable.Text> </TanStackTable.Text>;
|
||||
}
|
||||
const formatted =
|
||||
typeof timestamp === 'string'
|
||||
? formatTimezoneAdjustedTimestamp(
|
||||
timestamp,
|
||||
DATE_TIME_FORMATS.ISO_DATETIME_MS,
|
||||
)
|
||||
: formatTimezoneAdjustedTimestamp(
|
||||
timestamp / 1e6,
|
||||
DATE_TIME_FORMATS.ISO_DATETIME_MS,
|
||||
);
|
||||
return <TanStackTable.Text>{String(formatted)}</TanStackTable.Text>;
|
||||
},
|
||||
};
|
||||
|
||||
const fieldColumns = selectedColumns.map(
|
||||
(field): TableColumnDef<TraceListRow> => {
|
||||
const legacy = field as TelemetryFieldKey & LegacyFieldKey;
|
||||
const name = field?.name || legacy?.key || '';
|
||||
const fieldContext = field?.fieldContext || legacy?.type;
|
||||
|
||||
return {
|
||||
id: buildCompositeKey(name, fieldContext),
|
||||
header: name,
|
||||
accessorFn: (row): unknown => row?.[name],
|
||||
enableRemove: true,
|
||||
width: { min: 145 },
|
||||
cell: ({ value }): ReactElement => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return <TanStackTable.Text data-testid={name}>N/A</TanStackTable.Text>;
|
||||
}
|
||||
|
||||
if (BADGE_FIELDS.has(name)) {
|
||||
return (
|
||||
<Badge data-testid={name} color="sakura" variant="outline">
|
||||
{formatCellValue(value)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (DURATION_FIELDS.has(name)) {
|
||||
return (
|
||||
<TanStackTable.Text data-testid={name}>
|
||||
{getMs(formatCellValue(value))}ms
|
||||
</TanStackTable.Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span data-testid={name}>
|
||||
<LineClampedText text={formatCellValue(value)} lines={3} />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return [timestampColumn, ...fieldColumns];
|
||||
}, [selectedColumns, formatTimezoneAdjustedTimestamp]);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import { formUrlParams } from 'container/TraceDetail/utils';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
|
||||
import { formatCellValue, TraceListRow } from '../tableUtils';
|
||||
|
||||
/** Rows carry the span's attributes plus `date` (the list item's timestamp). */
|
||||
export const transformDataWithDate = (data: QueryDataV3[]): TraceListRow[] =>
|
||||
data[0]?.list?.map(({ data, timestamp }) => ({ ...data, date: timestamp })) ||
|
||||
[];
|
||||
|
||||
export const getTraceLink = (record: TraceListRow): string =>
|
||||
`${ROUTES.TRACE}/${formatCellValue(record.traceID || record.trace_id)}${formUrlParams(
|
||||
{
|
||||
spanId: record.spanID || record.span_id,
|
||||
levelUp: 0,
|
||||
levelDown: 0,
|
||||
},
|
||||
)}`;
|
||||
|
||||
/**
|
||||
* Row identity for the table. Spans are unique per row; the trace id is the
|
||||
* fallback for root-only rows.
|
||||
*/
|
||||
export const getTraceRowKey = (record: TraceListRow): string =>
|
||||
formatCellValue(
|
||||
record?.spanID ?? record?.span_id ?? record?.traceID ?? record?.trace_id,
|
||||
);
|
||||
@@ -0,0 +1,61 @@
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
||||
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
function QuerySection(): JSX.Element {
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
|
||||
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||
const isList = panelTypes === PANEL_TYPES.LIST;
|
||||
const config: QueryBuilderProps['filterConfigs'] = {
|
||||
stepInterval: { isHidden: false, isDisabled: false },
|
||||
limit: { isHidden: isList, isDisabled: true },
|
||||
having: { isHidden: isList, isDisabled: true },
|
||||
};
|
||||
|
||||
return config;
|
||||
}, [panelTypes]);
|
||||
|
||||
const renderOrderBy = useCallback(
|
||||
({ query, onChange }: OrderByFilterProps) => (
|
||||
<ExplorerOrderBy query={query} onChange={onChange} />
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const queryComponents = useMemo((): QueryBuilderProps['queryComponents'] => {
|
||||
const shouldRenderCustomOrderBy =
|
||||
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE;
|
||||
|
||||
return {
|
||||
...(shouldRenderCustomOrderBy ? { renderOrderBy } : {}),
|
||||
};
|
||||
}, [panelTypes, renderOrderBy]);
|
||||
|
||||
const isListViewPanel = useMemo(
|
||||
() => panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE,
|
||||
[panelTypes],
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryBuilderV2
|
||||
isListViewPanel={isListViewPanel}
|
||||
showTraceOperator
|
||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||
queryComponents={queryComponents}
|
||||
panelType={panelTypes}
|
||||
filterConfigs={filterConfigs}
|
||||
showOnlyWhereClause={
|
||||
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
|
||||
}
|
||||
version="v3"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(QuerySection);
|
||||
@@ -0,0 +1,7 @@
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: var(--spacing-6);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Space } from 'antd';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import ExportMenu from 'components/ExportMenu/ExportMenu';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { QueryTable } from 'container/QueryTable';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import styles from './TableView.module.scss';
|
||||
|
||||
function TableView({
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: {
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<QueryKey | undefined>;
|
||||
}): JSX.Element {
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
],
|
||||
[globalSelectedTime, maxTime, minTime, stagedQuery],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
|
||||
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap.traces,
|
||||
graphType: panelType || PANEL_TYPES.TABLE,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
params: {
|
||||
dataSource: 'traces',
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!stagedQuery && panelType === PANEL_TYPES.TABLE,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || isFetching) {
|
||||
setIsLoadingQueries(true);
|
||||
} else {
|
||||
setIsLoadingQueries(false);
|
||||
}
|
||||
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||
|
||||
const queryTableData = useMemo(
|
||||
() =>
|
||||
data?.payload?.data?.newResult?.data?.result ||
|
||||
data?.payload.data.result ||
|
||||
[],
|
||||
[data],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload) {
|
||||
setWarning(data.warning);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data?.payload, data?.warning]);
|
||||
|
||||
return (
|
||||
<Space.Compact block direction="vertical">
|
||||
{isError && error && <ErrorInPlace error={error as APIError} />}
|
||||
{!isError && data && (
|
||||
<div className={styles.header}>
|
||||
<ExportMenu
|
||||
dataSource={DataSource.TRACES}
|
||||
data={data}
|
||||
query={stagedQuery || initialQueriesMap.traces}
|
||||
fileName="traces-table"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isError && (
|
||||
<QueryTable
|
||||
query={stagedQuery || initialQueriesMap.traces}
|
||||
queryTableData={queryTableData as QueryDataV3[]}
|
||||
loading={isLoading}
|
||||
sticky
|
||||
/>
|
||||
)}
|
||||
</Space.Compact>
|
||||
);
|
||||
}
|
||||
|
||||
TableView.defaultProps = {
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default memo(TableView);
|
||||
@@ -0,0 +1,145 @@
|
||||
import {
|
||||
Dispatch,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
function TimeSeriesViewContainer({
|
||||
dataSource = DataSource.TRACES,
|
||||
isFilterApplied,
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: TimeSeriesViewProps): JSX.Element {
|
||||
const { stagedQuery, currentQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const isValidToConvertToMs = useMemo(() => {
|
||||
const isValid: boolean[] = [];
|
||||
|
||||
currentQuery.builder.queryData.forEach(
|
||||
({ aggregateAttribute, aggregateOperator }) => {
|
||||
const isExistDurationNanoAttribute =
|
||||
aggregateAttribute?.key === 'durationNano' ||
|
||||
aggregateAttribute?.key === 'duration_nano';
|
||||
|
||||
const isCountOperator =
|
||||
aggregateOperator === 'count' || aggregateOperator === 'count_distinct';
|
||||
|
||||
isValid.push(!isCountOperator && isExistDurationNanoAttribute);
|
||||
},
|
||||
);
|
||||
|
||||
return isValid.every(Boolean);
|
||||
}, [currentQuery]);
|
||||
|
||||
const defaultUnit = isValidToConvertToMs ? 'ms' : 'short';
|
||||
const { yAxisUnit, onUnitChange } = useUrlYAxisUnit(defaultUnit);
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
],
|
||||
[globalSelectedTime, maxTime, minTime, stagedQuery],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
|
||||
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap[dataSource],
|
||||
graphType: panelType || PANEL_TYPES.TIME_SERIES,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
params: {
|
||||
dataSource,
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!stagedQuery && panelType === PANEL_TYPES.TIME_SERIES,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload) {
|
||||
setWarning(data?.warning);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data?.payload, data?.warning]);
|
||||
|
||||
const responseData = useMemo(
|
||||
() => (isValidToConvertToMs ? convertDataValueToMs(data) : data),
|
||||
[data, isValidToConvertToMs],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || isFetching) {
|
||||
setIsLoadingQueries(true);
|
||||
} else {
|
||||
setIsLoadingQueries(false);
|
||||
}
|
||||
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TimeSeriesView
|
||||
isFilterApplied={isFilterApplied}
|
||||
isError={isError}
|
||||
error={error as APIError}
|
||||
isLoading={isLoading || isFetching}
|
||||
data={responseData}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onYAxisUnitChange={onUnitChange}
|
||||
dataSource={dataSource}
|
||||
setWarning={setWarning}
|
||||
allowExport
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TimeSeriesViewProps {
|
||||
dataSource?: DataSource;
|
||||
isFilterApplied: boolean;
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<QueryKey | undefined>;
|
||||
}
|
||||
|
||||
TimeSeriesViewContainer.defaultProps = {
|
||||
dataSource: DataSource.TRACES,
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default TimeSeriesViewContainer;
|
||||
@@ -0,0 +1,19 @@
|
||||
.loadingTraces {
|
||||
padding: var(--spacing-12) 0;
|
||||
height: 240px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gif {
|
||||
height: 72px;
|
||||
margin-left: calc(var(--spacing-12) * -1);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import loadingPlaneUrl from '@/assets/Icons/loading-plane.gif';
|
||||
|
||||
import styles from './TraceLoading.module.scss';
|
||||
|
||||
export function TracesLoading(): JSX.Element {
|
||||
const { t } = useTranslation('common');
|
||||
return (
|
||||
<div className={styles.loadingTraces}>
|
||||
<div className={styles.content}>
|
||||
<img className={styles.gif} src={loadingPlaneUrl} alt="wait-icon" />
|
||||
|
||||
<Typography>
|
||||
{t('pending_data_placeholder', { dataSource: DataSource.TRACES })}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// The traces explorer borrowed this rule from ListView's global stylesheet;
|
||||
// the fork keeps its own copy so the two can't drift into each other.
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
||||
import NoLogs from 'container/NoLogs/NoLogs';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import DOCLINKS from 'utils/docLinks';
|
||||
|
||||
import { PER_PAGE_OPTIONS } from '../constants';
|
||||
import ExplorerControls from '../Controls/Controls';
|
||||
import { getListViewQuery } from '../explorerUtils';
|
||||
import { TraceListRow } from '../tableUtils';
|
||||
import { TracesLoading } from '../TraceLoading/TraceLoading';
|
||||
import { columns } from './configs';
|
||||
import { ActionsContainer, Container } from './styles';
|
||||
import styles from './TracesView.module.scss';
|
||||
import { getRootSpanRowKey } from './utils';
|
||||
|
||||
interface TracesViewProps {
|
||||
isFilterApplied: boolean;
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<QueryKey | undefined>;
|
||||
}
|
||||
|
||||
function TracesView({
|
||||
isFilterApplied,
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: TracesViewProps): JSX.Element {
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
|
||||
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||
QueryParams.pagination,
|
||||
);
|
||||
|
||||
const transformedQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueriesMap.traces),
|
||||
[stagedQuery],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationQueryData,
|
||||
],
|
||||
[
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationQueryData,
|
||||
],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
|
||||
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
|
||||
{
|
||||
query: transformedQuery,
|
||||
graphType: panelType || PANEL_TYPES.TRACE,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
params: {
|
||||
dataSource: 'traces',
|
||||
},
|
||||
tableParams: {
|
||||
pagination: paginationQueryData,
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!stagedQuery && panelType === PANEL_TYPES.TRACE,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload) {
|
||||
setWarning(data?.warning);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data?.payload, data?.warning]);
|
||||
|
||||
const responseData = data?.payload?.data?.newResult?.data?.result[0]?.list;
|
||||
const tableData = useMemo(
|
||||
(): TraceListRow[] => responseData?.map((listItem) => listItem.data) ?? [],
|
||||
[responseData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || isFetching) {
|
||||
setIsLoadingQueries(true);
|
||||
} else {
|
||||
setIsLoadingQueries(false);
|
||||
}
|
||||
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isFetching && !isError && tableData.length !== 0) {
|
||||
void logEvent('AI Observability Explorer: Data present', {
|
||||
panelType: 'TRACE',
|
||||
});
|
||||
}
|
||||
}, [isLoading, isFetching, isError, panelType, tableData]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{tableData.length !== 0 && (
|
||||
<ActionsContainer>
|
||||
<Typography>
|
||||
This tab only shows Root Spans. More details
|
||||
<Typography.Link href={DOCLINKS.TRACES_DETAILS_LINK} target="_blank">
|
||||
{' '}
|
||||
here
|
||||
</Typography.Link>
|
||||
</Typography>
|
||||
|
||||
<div className={styles.controls}>
|
||||
<DownloadOptionsMenu
|
||||
dataSource={DataSource.TRACES}
|
||||
panelType={PANEL_TYPES.TRACE}
|
||||
/>
|
||||
|
||||
<ExplorerControls
|
||||
isLoading={isLoading}
|
||||
totalCount={responseData?.length || 0}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
</ActionsContainer>
|
||||
)}
|
||||
|
||||
{isError && error && <ErrorInPlace error={error as APIError} />}
|
||||
|
||||
{(isLoading || (isFetching && tableData.length === 0)) && <TracesLoading />}
|
||||
|
||||
{!isLoading &&
|
||||
!isFetching &&
|
||||
!isError &&
|
||||
!isFilterApplied &&
|
||||
tableData.length === 0 && <NoLogs dataSource={DataSource.TRACES} />}
|
||||
|
||||
{!isLoading &&
|
||||
!isFetching &&
|
||||
tableData.length === 0 &&
|
||||
!isError &&
|
||||
isFilterApplied && (
|
||||
<EmptyLogsSearch dataSource={DataSource.TRACES} panelType="TRACE" />
|
||||
)}
|
||||
|
||||
{tableData.length !== 0 && (
|
||||
<TanStackTable<TraceListRow>
|
||||
data={tableData}
|
||||
columns={columns}
|
||||
columnStorageKey={LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS}
|
||||
isLoading={isLoading}
|
||||
getRowKey={getRootSpanRowKey}
|
||||
// Rows are already page-sized by the query; virtualizing would need a
|
||||
// fixed-height container this page doesn't provide.
|
||||
disableVirtualScroll
|
||||
testId="ai-observability-traces-view-table"
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
TracesView.defaultProps = {
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default memo(TracesView);
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import { generatePath, Link } from 'react-router-dom';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||
|
||||
import { formatCellValue, TraceListRow } from '../tableUtils';
|
||||
|
||||
/**
|
||||
* Root-span columns. Fixed set — unlike the list view there are no user-selected
|
||||
* columns here, so nothing can be removed (there'd be no preference to sync to).
|
||||
*/
|
||||
export const columns: TableColumnDef<TraceListRow>[] = [
|
||||
{
|
||||
id: 'serviceName',
|
||||
header: 'Root Service Name',
|
||||
accessorFn: (row): unknown => row?.['service.name'],
|
||||
enableRemove: false,
|
||||
width: { min: 145 },
|
||||
cell: ({ value }): ReactElement => (
|
||||
<TanStackTable.Text>{formatCellValue(value)}</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
header: 'Root Operation Name',
|
||||
accessorFn: (row): unknown => row?.name,
|
||||
enableRemove: false,
|
||||
width: { min: 145 },
|
||||
cell: ({ value }): ReactElement => (
|
||||
<TanStackTable.Text>{formatCellValue(value)}</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'durationNano',
|
||||
header: 'Root Duration (in ms)',
|
||||
accessorFn: (row): unknown => row?.duration_nano,
|
||||
enableRemove: false,
|
||||
width: { min: 145 },
|
||||
cell: ({ value }): ReactElement => (
|
||||
<TanStackTable.Text>
|
||||
{value === undefined || value === null
|
||||
? ''
|
||||
: `${getMs(formatCellValue(value))}ms`}
|
||||
</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'span_count',
|
||||
header: 'No of Spans',
|
||||
accessorFn: (row): unknown => row?.span_count,
|
||||
enableRemove: false,
|
||||
width: { min: 145 },
|
||||
cell: ({ value }): ReactElement => (
|
||||
<TanStackTable.Text>{formatCellValue(value)}</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'traceID',
|
||||
header: 'TraceID',
|
||||
accessorFn: (row): unknown => row?.trace_id,
|
||||
enableRemove: false,
|
||||
width: { min: 145 },
|
||||
cell: ({ value }): ReactElement => {
|
||||
const traceID = formatCellValue(value);
|
||||
if (!traceID) {
|
||||
return <TanStackTable.Text> </TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
to={generatePath(ROUTES.TRACE_DETAIL, { id: traceID })}
|
||||
data-testid="trace-id"
|
||||
>
|
||||
{traceID}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const ActionsContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { formatCellValue, TraceListRow } from '../tableUtils';
|
||||
|
||||
/**
|
||||
* Row identity for the root-span table: one row per trace. Tolerates the
|
||||
* skeleton placeholder rows the table renders before the first response.
|
||||
*/
|
||||
export const getRootSpanRowKey = (record: TraceListRow): string =>
|
||||
formatCellValue(record?.trace_id ?? record?.traceID);
|
||||
@@ -0,0 +1,49 @@
|
||||
import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
|
||||
|
||||
export const TOOLBAR_VIEWS = {
|
||||
list: {
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
show: true,
|
||||
key: 'list',
|
||||
},
|
||||
timeseries: {
|
||||
name: 'timeseries',
|
||||
label: 'Timeseries',
|
||||
disabled: false,
|
||||
show: true,
|
||||
key: 'timeseries',
|
||||
},
|
||||
trace: {
|
||||
name: 'trace',
|
||||
label: 'Trace',
|
||||
disabled: false,
|
||||
show: true,
|
||||
key: 'trace',
|
||||
},
|
||||
table: {
|
||||
name: 'table',
|
||||
label: 'Table',
|
||||
disabled: false,
|
||||
show: true,
|
||||
key: 'table',
|
||||
},
|
||||
clickhouse: {
|
||||
name: 'clickhouse',
|
||||
label: 'Clickhouse',
|
||||
disabled: false,
|
||||
show: false,
|
||||
key: 'clickhouse',
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultSelectedColumns: string[] = [
|
||||
'service.name',
|
||||
'name',
|
||||
'duration_nano',
|
||||
'http_method',
|
||||
'response_status_code',
|
||||
'timestamp',
|
||||
];
|
||||
|
||||
export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS];
|
||||
@@ -0,0 +1,75 @@
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { cloneDeep, set } from 'lodash-es';
|
||||
import { OrderByPayload, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const getListViewQuery = (
|
||||
stagedQuery: Query,
|
||||
orderBy?: string,
|
||||
): Query => {
|
||||
const query = stagedQuery
|
||||
? cloneDeep(stagedQuery)
|
||||
: cloneDeep(initialQueriesMap.traces);
|
||||
|
||||
const orderByPayload: OrderByPayload[] = orderBy
|
||||
? [
|
||||
{
|
||||
columnName: orderBy.split(':')[0],
|
||||
order: orderBy.split(':')[1] as 'asc' | 'desc',
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
for (let i = 0; i < query.builder.queryData.length; i++) {
|
||||
const queryData = query.builder.queryData[i];
|
||||
queryData.groupBy = [];
|
||||
queryData.having = {
|
||||
expression: '',
|
||||
};
|
||||
queryData.orderBy = orderByPayload;
|
||||
}
|
||||
|
||||
if (
|
||||
query.builder.queryTraceOperator &&
|
||||
query.builder.queryTraceOperator.length > 0
|
||||
) {
|
||||
for (let i = 0; i < query.builder.queryTraceOperator.length; i++) {
|
||||
const queryTraceOperator = query.builder.queryTraceOperator[i];
|
||||
queryTraceOperator.groupBy = [];
|
||||
queryTraceOperator.having = {
|
||||
expression: '',
|
||||
};
|
||||
queryTraceOperator.orderBy = orderByPayload;
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
export const getQueryByPanelType = (
|
||||
stagedQuery: Query,
|
||||
panelType: PANEL_TYPES,
|
||||
): Query => {
|
||||
if (panelType === PANEL_TYPES.LIST || panelType === PANEL_TYPES.TRACE) {
|
||||
return getListViewQuery(stagedQuery);
|
||||
}
|
||||
return stagedQuery;
|
||||
};
|
||||
|
||||
export const getExportQueryData = (
|
||||
query: Query,
|
||||
panelType: PANEL_TYPES,
|
||||
options: OptionsQuery,
|
||||
): Query => {
|
||||
if (panelType === PANEL_TYPES.LIST) {
|
||||
const updatedQuery = cloneDeep(query);
|
||||
set(
|
||||
updatedQuery,
|
||||
'builder.queryData[0].selectColumns',
|
||||
options.selectColumns,
|
||||
);
|
||||
|
||||
return updatedQuery;
|
||||
}
|
||||
return query;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* A table row: the span's flattened attributes. Which keys are present depends
|
||||
* on the query and the selected columns, so every field access has to tolerate
|
||||
* `undefined` — including the skeleton placeholder rows the table renders
|
||||
* before the first response lands.
|
||||
*/
|
||||
export type TraceListRow = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Renders a dynamic row value as text. Values are whatever the span carried, so
|
||||
* objects (a JSON body, say) are serialised rather than left to stringify to
|
||||
* `[object Object]`.
|
||||
*/
|
||||
export const formatCellValue = (value: unknown): string => {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return String(value);
|
||||
}
|
||||
if (value !== null && typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
@@ -18,6 +18,13 @@ jest.mock('pages/DashboardPageV2/DashboardContainer', () => ({
|
||||
default: (): JSX.Element => <div data-testid="llm-overview-dashboard" />,
|
||||
}));
|
||||
|
||||
// Same for the Explorer tab, which renders a full query-builder surface
|
||||
// (toolbar + date picker).
|
||||
jest.mock('container/LLMObservability/Explorer/Explorer', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="llm-observability-explorer" />,
|
||||
}));
|
||||
|
||||
function setupList(items = mockRules): void {
|
||||
server.use(
|
||||
rest.get(LLM_PRICING_ENDPOINT, (_req, res, ctx) =>
|
||||
|
||||
@@ -17,8 +17,6 @@ import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import {
|
||||
getHostQueryPayload,
|
||||
getNodeQueryPayload,
|
||||
@@ -53,23 +51,12 @@ function NodeMetrics({
|
||||
};
|
||||
}, [timestamp]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const queryPayloads = useMemo(() => {
|
||||
if (nodeName) {
|
||||
return getNodeQueryPayload(
|
||||
clusterName,
|
||||
nodeName,
|
||||
start,
|
||||
end,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
return getNodeQueryPayload(clusterName, nodeName, start, end);
|
||||
}
|
||||
return getHostQueryPayload(hostName, start, end, dotMetricsEnabled);
|
||||
}, [nodeName, hostName, clusterName, start, end, dotMetricsEnabled]);
|
||||
return getHostQueryPayload(hostName, start, end);
|
||||
}, [nodeName, hostName, clusterName, start, end]);
|
||||
|
||||
const widgetInfo = nodeName ? nodeWidgetInfo : hostWidgetInfo;
|
||||
const queries = useQueries(
|
||||
|
||||
@@ -12,13 +12,11 @@ import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { getPodQueryPayload, podWidgetInfo } from './constants';
|
||||
|
||||
function PodMetrics({
|
||||
@@ -54,14 +52,9 @@ function PodMetrics({
|
||||
scrollLeft: 0,
|
||||
});
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const queryPayloads = useMemo(
|
||||
() => getPodQueryPayload(clusterName, podName, start, end, dotMetricsEnabled),
|
||||
[clusterName, end, podName, start, dotMetricsEnabled],
|
||||
() => getPodQueryPayload(clusterName, podName, start, end),
|
||||
[clusterName, end, podName, start],
|
||||
);
|
||||
const queries = useQueries(
|
||||
queryPayloads.map((payload) => ({
|
||||
|
||||
@@ -1,56 +1,39 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import type { Having } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import type { Having as HavingV5 } from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
|
||||
const buildSumGreaterThanZeroHaving = (
|
||||
metricKey: string,
|
||||
useV5HavingFormat: boolean,
|
||||
): Having[] | HavingV5 =>
|
||||
useV5HavingFormat
|
||||
? { expression: `sum(${metricKey}) > 0` }
|
||||
: [{ columnName: `SUM(${metricKey})`, op: '>', value: 0 }];
|
||||
|
||||
export const getPodQueryPayload = (
|
||||
clusterName: string,
|
||||
podName: string,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
): GetQueryResultsProps[] => {
|
||||
const k8sClusterNameKey = dotMetricsEnabled
|
||||
? 'k8s.cluster.name'
|
||||
: 'k8s_cluster_name';
|
||||
const k8sPodNameKey = dotMetricsEnabled ? 'k8s.pod.name' : 'k8s_pod_name';
|
||||
const containerCpuUtilKey = dotMetricsEnabled
|
||||
? 'container.cpu.usage'
|
||||
: 'container_cpu_usage';
|
||||
const containerMemUsageKey = dotMetricsEnabled
|
||||
? 'container.memory.usage'
|
||||
: 'container_memory_usage';
|
||||
const k8sContainerCpuReqKey = dotMetricsEnabled
|
||||
? 'k8s.container.cpu_request'
|
||||
: 'k8s_container_cpu_request';
|
||||
const k8sContainerCpuLimitKey = dotMetricsEnabled
|
||||
? 'k8s.container.cpu_limit'
|
||||
: 'k8s_container_cpu_limit';
|
||||
const k8sContainerMemReqKey = dotMetricsEnabled
|
||||
? 'k8s.container.memory_request'
|
||||
: 'k8s_container_memory_request';
|
||||
const k8sContainerMemLimitKey = dotMetricsEnabled
|
||||
? 'k8s.container.memory_limit'
|
||||
: 'k8s_container_memory_limit';
|
||||
const k8sPodFsAvailKey = dotMetricsEnabled
|
||||
? 'k8s.pod.filesystem.available'
|
||||
: 'k8s_pod_filesystem_available';
|
||||
const k8sPodFsCapKey = dotMetricsEnabled
|
||||
? 'k8s.pod.filesystem.capacity'
|
||||
: 'k8s_pod_filesystem_capacity';
|
||||
const k8sPodNetIoKey = dotMetricsEnabled
|
||||
? 'k8s.pod.network.io'
|
||||
: 'k8s_pod_network_io';
|
||||
const podLegendTemplate = dotMetricsEnabled
|
||||
? '{{k8s.pod.name}}'
|
||||
: '{{k8s_pod_name}}';
|
||||
const podLegendUsage = dotMetricsEnabled
|
||||
? 'usage - {{k8s.pod.name}}'
|
||||
: 'usage - {{k8s_pod_name}}';
|
||||
const podLegendLimit = dotMetricsEnabled
|
||||
? 'limit - {{k8s.pod.name}}'
|
||||
: 'limit - {{k8s_pod_name}}';
|
||||
const k8sClusterNameKey = 'k8s.cluster.name';
|
||||
const k8sPodNameKey = 'k8s.pod.name';
|
||||
const containerCpuUtilKey = 'container.cpu.usage';
|
||||
const containerMemUsageKey = 'container.memory.usage';
|
||||
const k8sContainerCpuReqKey = 'k8s.container.cpu_request';
|
||||
const k8sContainerCpuLimitKey = 'k8s.container.cpu_limit';
|
||||
const k8sContainerMemReqKey = 'k8s.container.memory_request';
|
||||
const k8sContainerMemLimitKey = 'k8s.container.memory_limit';
|
||||
const k8sPodFsAvailKey = 'k8s.pod.filesystem.available';
|
||||
const k8sPodFsCapKey = 'k8s.pod.filesystem.capacity';
|
||||
const k8sPodNetIoKey = 'k8s.pod.network.io';
|
||||
const podLegendTemplate = '{{k8s.pod.name}}';
|
||||
const podLegendUsage = 'usage - {{k8s.pod.name}}';
|
||||
const podLegendLimit = 'limit - {{k8s.pod.name}}';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -1027,36 +1010,17 @@ export const getNodeQueryPayload = (
|
||||
nodeName: string,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
): GetQueryResultsProps[] => {
|
||||
const k8sClusterNameKey = dotMetricsEnabled
|
||||
? 'k8s.cluster.name'
|
||||
: 'k8s_cluster_name';
|
||||
const k8sNodeNameKey = dotMetricsEnabled ? 'k8s.node.name' : 'k8s_node_name';
|
||||
const k8sNodeCpuTimeKey = dotMetricsEnabled
|
||||
? 'k8s.node.cpu.time'
|
||||
: 'k8s_node_cpu_time';
|
||||
const k8sNodeAllocCpuKey = dotMetricsEnabled
|
||||
? 'k8s.node.allocatable_cpu'
|
||||
: 'k8s_node_allocatable_cpu';
|
||||
const k8sNodeMemWsKey = dotMetricsEnabled
|
||||
? 'k8s.node.memory.working_set'
|
||||
: 'k8s_node_memory_working_set';
|
||||
const k8sNodeAllocMemKey = dotMetricsEnabled
|
||||
? 'k8s.node.allocatable_memory'
|
||||
: 'k8s_node_allocatable_memory';
|
||||
const k8sNodeNetIoKey = dotMetricsEnabled
|
||||
? 'k8s.node.network.io'
|
||||
: 'k8s_node_network_io';
|
||||
const k8sNodeFsAvailKey = dotMetricsEnabled
|
||||
? 'k8s.node.filesystem.available'
|
||||
: 'k8s_node_filesystem_available';
|
||||
const k8sNodeFsCapKey = dotMetricsEnabled
|
||||
? 'k8s.node.filesystem.capacity'
|
||||
: 'k8s_node_filesystem_capacity';
|
||||
const podLegend = dotMetricsEnabled
|
||||
? '{{k8s.node.name}}'
|
||||
: '{{k8s_node_name}}';
|
||||
const k8sClusterNameKey = 'k8s.cluster.name';
|
||||
const k8sNodeNameKey = 'k8s.node.name';
|
||||
const k8sNodeCpuTimeKey = 'k8s.node.cpu.time';
|
||||
const k8sNodeAllocCpuKey = 'k8s.node.allocatable_cpu';
|
||||
const k8sNodeMemWsKey = 'k8s.node.memory.working_set';
|
||||
const k8sNodeAllocMemKey = 'k8s.node.allocatable_memory';
|
||||
const k8sNodeNetIoKey = 'k8s.node.network.io';
|
||||
const k8sNodeFsAvailKey = 'k8s.node.filesystem.available';
|
||||
const k8sNodeFsCapKey = 'k8s.node.filesystem.capacity';
|
||||
const podLegend = '{{k8s.node.name}}';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -1586,48 +1550,24 @@ export const getHostQueryPayload = (
|
||||
hostName: string,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
useV5HavingFormat = false,
|
||||
): GetQueryResultsProps[] => {
|
||||
const hostNameKey = dotMetricsEnabled ? 'host.name' : 'host_name';
|
||||
const cpuTimeKey = dotMetricsEnabled ? 'system.cpu.time' : 'system_cpu_time';
|
||||
const memUsageKey = dotMetricsEnabled
|
||||
? 'system.memory.usage'
|
||||
: 'system_memory_usage';
|
||||
const load1mKey = dotMetricsEnabled
|
||||
? 'system.cpu.load_average.1m'
|
||||
: 'system_cpu_load_average_1m';
|
||||
const load5mKey = dotMetricsEnabled
|
||||
? 'system.cpu.load_average.5m'
|
||||
: 'system_cpu_load_average_5m';
|
||||
const load15mKey = dotMetricsEnabled
|
||||
? 'system.cpu.load_average.15m'
|
||||
: 'system_cpu_load_average_15m';
|
||||
const netIoKey = dotMetricsEnabled ? 'system.network.io' : 'system_network_io';
|
||||
const netPktsKey = dotMetricsEnabled
|
||||
? 'system.network.packets'
|
||||
: 'system_network_packets';
|
||||
const netErrKey = dotMetricsEnabled
|
||||
? 'system.network.errors'
|
||||
: 'system_network_errors';
|
||||
const netDropKey = dotMetricsEnabled
|
||||
? 'system.network.dropped'
|
||||
: 'system_network_dropped';
|
||||
const netConnKey = dotMetricsEnabled
|
||||
? 'system.network.connections'
|
||||
: 'system_network_connections';
|
||||
const diskIoKey = dotMetricsEnabled ? 'system.disk.io' : 'system_disk_io';
|
||||
const diskOpTimeKey = dotMetricsEnabled
|
||||
? 'system.disk.operation_time'
|
||||
: 'system_disk_operation_time';
|
||||
const diskOpsKey = dotMetricsEnabled
|
||||
? 'system.disk.operations'
|
||||
: 'system_disk_operations';
|
||||
const diskPendingKey = dotMetricsEnabled
|
||||
? 'system.disk.pending_operations'
|
||||
: 'system_disk_pending_operations';
|
||||
const fsUsageKey = dotMetricsEnabled
|
||||
? 'system.filesystem.usage'
|
||||
: 'system_filesystem_usage';
|
||||
const hostNameKey = 'host.name';
|
||||
const cpuTimeKey = 'system.cpu.time';
|
||||
const memUsageKey = 'system.memory.usage';
|
||||
const load1mKey = 'system.cpu.load_average.1m';
|
||||
const load5mKey = 'system.cpu.load_average.5m';
|
||||
const load15mKey = 'system.cpu.load_average.15m';
|
||||
const netIoKey = 'system.network.io';
|
||||
const netPktsKey = 'system.network.packets';
|
||||
const netErrKey = 'system.network.errors';
|
||||
const netDropKey = 'system.network.dropped';
|
||||
const netConnKey = 'system.network.connections';
|
||||
const diskIoKey = 'system.disk.io';
|
||||
const diskOpTimeKey = 'system.disk.operation_time';
|
||||
const diskOpsKey = 'system.disk.operations';
|
||||
const diskPendingKey = 'system.disk.pending_operations';
|
||||
const fsUsageKey = 'system.filesystem.usage';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -1873,13 +1813,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${fsUsageKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(fsUsageKey, useV5HavingFormat),
|
||||
legend: '{{mountpoint}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -1928,13 +1862,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${fsUsageKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(fsUsageKey, useV5HavingFormat),
|
||||
legend: '{{mountpoint}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -2160,13 +2088,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${netIoKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(netIoKey, useV5HavingFormat),
|
||||
legend: '{{device}}::{{direction}}',
|
||||
limit: 30,
|
||||
orderBy: [],
|
||||
@@ -2622,13 +2544,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${diskOpsKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(diskOpsKey, useV5HavingFormat),
|
||||
legend: '{{device}}::{{direction}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -2697,13 +2613,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${diskPendingKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(diskPendingKey, useV5HavingFormat),
|
||||
legend: '{{device}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -2779,13 +2689,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${diskOpTimeKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(diskOpTimeKey, useV5HavingFormat),
|
||||
legend: '{{device}}::{{direction}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
|
||||
@@ -21,7 +21,6 @@ export const databaseCallsRPS = ({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}: DatabaseCallsRPSProps): QueryBuilderData => {
|
||||
const autocompleteData: BaseAutocompleteData[] = [
|
||||
{
|
||||
@@ -33,7 +32,7 @@ export const databaseCallsRPS = ({
|
||||
const groupBy: BaseAutocompleteData[] = [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled ? WidgetKeys.Db_system : WidgetKeys.Db_system_norm,
|
||||
key: WidgetKeys.DbSystem,
|
||||
type: 'tag',
|
||||
},
|
||||
];
|
||||
@@ -42,9 +41,7 @@ export const databaseCallsRPS = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
@@ -75,7 +72,6 @@ export const databaseCallsRPS = ({
|
||||
export const databaseCallsAvgDuration = ({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}: DatabaseCallProps): QueryBuilderData => {
|
||||
const autocompleteDataA: BaseAutocompleteData = {
|
||||
key: WidgetKeys.SignozDbLatencySum,
|
||||
@@ -92,9 +88,7 @@ export const databaseCallsAvgDuration = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
|
||||
@@ -32,7 +32,6 @@ export const externalCallErrorPercent = ({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}: ExternalCallDurationByAddressProps): QueryBuilderData => {
|
||||
const autocompleteDataA: BaseAutocompleteData = {
|
||||
key: WidgetKeys.SignozExternalCallLatencyCount,
|
||||
@@ -49,9 +48,7 @@ export const externalCallErrorPercent = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
@@ -61,7 +58,7 @@ export const externalCallErrorPercent = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
|
||||
key: WidgetKeys.StatusCode,
|
||||
dataType: DataTypes.Int64,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
@@ -74,9 +71,7 @@ export const externalCallErrorPercent = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
@@ -120,7 +115,6 @@ export const externalCallErrorPercent = ({
|
||||
export const externalCallDuration = ({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}: ExternalCallProps): QueryBuilderData => {
|
||||
const autocompleteDataA: BaseAutocompleteData = {
|
||||
dataType: DataTypes.Float64,
|
||||
@@ -141,9 +135,7 @@ export const externalCallDuration = ({
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -183,7 +175,6 @@ export const externalCallRpsByAddress = ({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}: ExternalCallDurationByAddressProps): QueryBuilderData => {
|
||||
const autocompleteData: BaseAutocompleteData[] = [
|
||||
{
|
||||
@@ -198,9 +189,7 @@ export const externalCallRpsByAddress = ({
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -231,7 +220,6 @@ export const externalCallDurationByAddress = ({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}: ExternalCallDurationByAddressProps): QueryBuilderData => {
|
||||
const autocompleteDataA: BaseAutocompleteData = {
|
||||
dataType: DataTypes.Float64,
|
||||
@@ -251,9 +239,7 @@ export const externalCallDurationByAddress = ({
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
|
||||
@@ -37,15 +37,10 @@ export const latency = ({
|
||||
tagFilterItems,
|
||||
isSpanMetricEnable = false,
|
||||
topLevelOperationsRoute,
|
||||
dotMetricsEnabled,
|
||||
}: LatencyProps): QueryBuilderData => {
|
||||
const signozLatencyBucketMetrics = dotMetricsEnabled
|
||||
? WidgetKeys.Signoz_latency_bucket
|
||||
: WidgetKeys.Signoz_latency_bucket_norm;
|
||||
const signozLatencyBucketMetrics = WidgetKeys.SignozLatencyBucket;
|
||||
|
||||
const signozMetricsServiceName = dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm;
|
||||
const signozMetricsServiceName = WidgetKeys.OTelServiceName;
|
||||
const newAutoCompleteData: BaseAutocompleteData = {
|
||||
key: isSpanMetricEnable
|
||||
? signozLatencyBucketMetrics
|
||||
@@ -287,28 +282,21 @@ export const apDexMetricsQueryBuilderQueries = ({
|
||||
threashold,
|
||||
delta,
|
||||
metricsBuckets,
|
||||
dotMetricsEnabled,
|
||||
}: ApDexMetricsQueryBuilderQueriesProps): QueryBuilderData => {
|
||||
const autoCompleteDataA: BaseAutocompleteData = {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.SignozLatencyCount
|
||||
: WidgetKeys.SignozLatencyCountNorm,
|
||||
key: WidgetKeys.SignozLatencyCount,
|
||||
dataType: DataTypes.Float64,
|
||||
type: '',
|
||||
};
|
||||
|
||||
const autoCompleteDataB: BaseAutocompleteData = {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Signoz_latency_bucket
|
||||
: WidgetKeys.Signoz_latency_bucket_norm,
|
||||
key: WidgetKeys.SignozLatencyBucket,
|
||||
dataType: DataTypes.Float64,
|
||||
type: '',
|
||||
};
|
||||
|
||||
const autoCompleteDataC: BaseAutocompleteData = {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Signoz_latency_bucket
|
||||
: WidgetKeys.Signoz_latency_bucket_norm,
|
||||
key: WidgetKeys.SignozLatencyBucket,
|
||||
dataType: DataTypes.Float64,
|
||||
type: '',
|
||||
};
|
||||
@@ -317,9 +305,7 @@ export const apDexMetricsQueryBuilderQueries = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
@@ -343,7 +329,7 @@ export const apDexMetricsQueryBuilderQueries = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
|
||||
key: WidgetKeys.StatusCode,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
@@ -363,9 +349,7 @@ export const apDexMetricsQueryBuilderQueries = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
@@ -399,7 +383,7 @@ export const apDexMetricsQueryBuilderQueries = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
|
||||
key: WidgetKeys.StatusCode,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
@@ -409,9 +393,7 @@ export const apDexMetricsQueryBuilderQueries = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
@@ -474,13 +456,10 @@ export const operationPerSec = ({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
topLevelOperations,
|
||||
dotMetricsEnabled,
|
||||
}: OperationPerSecProps): QueryBuilderData => {
|
||||
const autocompleteData: BaseAutocompleteData[] = [
|
||||
{
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.SignozLatencyCount
|
||||
: WidgetKeys.SignozLatencyCountNorm,
|
||||
key: WidgetKeys.SignozLatencyCount,
|
||||
dataType: DataTypes.Float64,
|
||||
type: '',
|
||||
},
|
||||
@@ -491,9 +470,7 @@ export const operationPerSec = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
@@ -534,7 +511,6 @@ export const errorPercentage = ({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
topLevelOperations,
|
||||
dotMetricsEnabled,
|
||||
}: OperationPerSecProps): QueryBuilderData => {
|
||||
const autocompleteDataA: BaseAutocompleteData = {
|
||||
key: WidgetKeys.SignozCallsTotal,
|
||||
@@ -553,9 +529,7 @@ export const errorPercentage = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
@@ -575,7 +549,7 @@ export const errorPercentage = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
|
||||
key: WidgetKeys.StatusCode,
|
||||
dataType: DataTypes.Int64,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
@@ -589,9 +563,7 @@ export const errorPercentage = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
|
||||
@@ -21,12 +21,9 @@ import { getQueryBuilderQuerieswithFormula } from './MetricsPageQueriesFactory';
|
||||
|
||||
export const topOperationQueries = ({
|
||||
servicename,
|
||||
dotMetricsEnabled,
|
||||
}: TopOperationQueryFactoryProps): QueryBuilderData => {
|
||||
const latencyAutoCompleteData: BaseAutocompleteData = {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Signoz_latency_bucket
|
||||
: WidgetKeys.Signoz_latency_bucket_norm,
|
||||
key: WidgetKeys.SignozLatencyBucket,
|
||||
dataType: DataTypes.Float64,
|
||||
type: '',
|
||||
};
|
||||
@@ -38,9 +35,7 @@ export const topOperationQueries = ({
|
||||
};
|
||||
|
||||
const numOfCallAutoCompleteData: BaseAutocompleteData = {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.SignozLatencyCount
|
||||
: WidgetKeys.SignozLatencyCountNorm,
|
||||
key: WidgetKeys.SignozLatencyCount,
|
||||
dataType: DataTypes.Float64,
|
||||
type: '',
|
||||
};
|
||||
@@ -49,9 +44,7 @@ export const topOperationQueries = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
@@ -65,9 +58,7 @@ export const topOperationQueries = ({
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -77,7 +68,7 @@ export const topOperationQueries = ({
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.Int64,
|
||||
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
|
||||
key: WidgetKeys.StatusCode,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
|
||||
@@ -28,8 +28,6 @@ import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import {
|
||||
GraphTitle,
|
||||
MENU_ITEMS,
|
||||
@@ -89,12 +87,7 @@ function DBCall(): JSX.Element {
|
||||
[queries],
|
||||
);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const legend = dotMetricsEnabled ? '{{db.system}}' : '{{db_system}}';
|
||||
const legend = '{{db.system}}';
|
||||
|
||||
const databaseCallsRPSWidget = useMemo(
|
||||
() =>
|
||||
@@ -106,7 +99,6 @@ function DBCall(): JSX.Element {
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -117,7 +109,7 @@ function DBCall(): JSX.Element {
|
||||
id: SERVICE_CHART_ID.dbCallsRPS,
|
||||
fillSpans: false,
|
||||
}),
|
||||
[servicename, tagFilterItems, dotMetricsEnabled, legend],
|
||||
[servicename, tagFilterItems, legend],
|
||||
);
|
||||
const databaseCallsAverageDurationWidget = useMemo(
|
||||
() =>
|
||||
@@ -128,7 +120,6 @@ function DBCall(): JSX.Element {
|
||||
builder: databaseCallsAvgDuration({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -139,7 +130,7 @@ function DBCall(): JSX.Element {
|
||||
id: GraphTitle.DATABASE_CALLS_AVG_DURATION,
|
||||
fillSpans: true,
|
||||
}),
|
||||
[servicename, tagFilterItems, dotMetricsEnabled],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const stepInterval = useMemo(
|
||||
@@ -157,7 +148,7 @@ function DBCall(): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(),
|
||||
)?.tagValue;
|
||||
|
||||
logEvent('APM: Service detail page visited', {
|
||||
|
||||
@@ -30,8 +30,6 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import {
|
||||
GraphTitle,
|
||||
legend,
|
||||
@@ -84,10 +82,6 @@ function External(): JSX.Element {
|
||||
handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [],
|
||||
[queries],
|
||||
);
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const externalCallErrorWidget = useMemo(
|
||||
() =>
|
||||
@@ -99,7 +93,6 @@ function External(): JSX.Element {
|
||||
servicename,
|
||||
legend: legend.address,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -109,7 +102,7 @@ function External(): JSX.Element {
|
||||
yAxisUnit: '%',
|
||||
id: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
|
||||
}),
|
||||
[servicename, tagFilterItems, dotMetricsEnabled],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const selectedTraceTags = useMemo(
|
||||
@@ -126,7 +119,6 @@ function External(): JSX.Element {
|
||||
builder: externalCallDuration({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -137,7 +129,7 @@ function External(): JSX.Element {
|
||||
id: GraphTitle.EXTERNAL_CALL_DURATION,
|
||||
fillSpans: true,
|
||||
}),
|
||||
[servicename, tagFilterItems, dotMetricsEnabled],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const errorApmToTraceQuery = useGetAPMToTracesQueries({
|
||||
@@ -171,7 +163,7 @@ function External(): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(),
|
||||
)?.tagValue;
|
||||
|
||||
logEvent('APM: Service detail page visited', {
|
||||
@@ -194,7 +186,6 @@ function External(): JSX.Element {
|
||||
servicename,
|
||||
legend: legend.address,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -205,7 +196,7 @@ function External(): JSX.Element {
|
||||
id: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
|
||||
fillSpans: true,
|
||||
}),
|
||||
[servicename, tagFilterItems, dotMetricsEnabled],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const externalCallDurationAddressWidget = useMemo(
|
||||
@@ -218,7 +209,6 @@ function External(): JSX.Element {
|
||||
servicename,
|
||||
legend: legend.address,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -229,7 +219,7 @@ function External(): JSX.Element {
|
||||
id: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
|
||||
fillSpans: true,
|
||||
}),
|
||||
[servicename, tagFilterItems, dotMetricsEnabled],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const apmToTraceQuery = useGetAPMToTracesQueries({
|
||||
|
||||
@@ -93,15 +93,12 @@ function Application(): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[handleSetTimeStamp],
|
||||
);
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(),
|
||||
)?.tagValue;
|
||||
|
||||
logEvent('APM: Service detail page visited', {
|
||||
@@ -159,7 +156,6 @@ function Application(): JSX.Element {
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
topLevelOperations: topLevelOperationsRoute,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -169,7 +165,7 @@ function Application(): JSX.Element {
|
||||
yAxisUnit: 'ops',
|
||||
id: SERVICE_CHART_ID.rps,
|
||||
}),
|
||||
[servicename, tagFilterItems, topLevelOperationsRoute, dotMetricsEnabled],
|
||||
[servicename, tagFilterItems, topLevelOperationsRoute],
|
||||
);
|
||||
|
||||
const errorPercentageWidget = useMemo(
|
||||
@@ -182,7 +178,6 @@ function Application(): JSX.Element {
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
topLevelOperations: topLevelOperationsRoute,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -193,7 +188,7 @@ function Application(): JSX.Element {
|
||||
id: SERVICE_CHART_ID.errorPercentage,
|
||||
fillSpans: true,
|
||||
}),
|
||||
[servicename, tagFilterItems, topLevelOperationsRoute, dotMetricsEnabled],
|
||||
[servicename, tagFilterItems, topLevelOperationsRoute],
|
||||
);
|
||||
|
||||
const stepInterval = useMemo(
|
||||
|
||||
@@ -22,8 +22,6 @@ import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/Me
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../../../../constants/features';
|
||||
import { useAppContext } from '../../../../../providers/App/App';
|
||||
import { IServiceName } from '../../types';
|
||||
import { ApDexMetricsProps } from './types';
|
||||
|
||||
@@ -38,10 +36,6 @@ function ApDexMetrics({
|
||||
}: ApDexMetricsProps): JSX.Element {
|
||||
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
||||
const servicename = decodeURIComponent(encodedServiceName);
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
const apDexMetricsWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
@@ -55,7 +49,6 @@ function ApDexMetrics({
|
||||
threashold: thresholdValue || 0,
|
||||
delta: delta || false,
|
||||
metricsBuckets: metricsBuckets || [],
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -81,7 +74,6 @@ function ApDexMetrics({
|
||||
tagFilterItems,
|
||||
thresholdValue,
|
||||
topLevelOperationsRoute,
|
||||
dotMetricsEnabled,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ import Spinner from 'components/Spinner';
|
||||
import { useGetMetricMeta } from 'hooks/apDex/useGetMetricMeta';
|
||||
import useErrorNotification from 'hooks/useErrorNotification';
|
||||
|
||||
import { FeatureKeys } from '../../../../../constants/features';
|
||||
import { useAppContext } from '../../../../../providers/App/App';
|
||||
import { WidgetKeys } from '../../../constant';
|
||||
import { IServiceName } from '../../types';
|
||||
import ApDexMetrics from './ApDexMetrics';
|
||||
@@ -20,17 +18,8 @@ function ApDexMetricsApplication({
|
||||
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
||||
const servicename = decodeURIComponent(encodedServiceName);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const signozLatencyBucketMetrics = dotMetricsEnabled
|
||||
? WidgetKeys.Signoz_latency_bucket
|
||||
: WidgetKeys.Signoz_latency_bucket_norm;
|
||||
|
||||
const { data, isLoading, error } = useGetMetricMeta(
|
||||
signozLatencyBucketMetrics,
|
||||
WidgetKeys.SignozLatencyBucket,
|
||||
servicename,
|
||||
);
|
||||
useErrorNotification(error);
|
||||
|
||||
@@ -56,10 +56,6 @@ function ServiceOverview({
|
||||
[isSpanMetricEnable, queries],
|
||||
);
|
||||
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const latencyWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
@@ -71,7 +67,6 @@ function ServiceOverview({
|
||||
tagFilterItems,
|
||||
isSpanMetricEnable,
|
||||
topLevelOperationsRoute,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -81,13 +76,7 @@ function ServiceOverview({
|
||||
yAxisUnit: 'ns',
|
||||
id: SERVICE_CHART_ID.latency,
|
||||
}),
|
||||
[
|
||||
isSpanMetricEnable,
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
topLevelOperationsRoute,
|
||||
dotMetricsEnabled,
|
||||
],
|
||||
[isSpanMetricEnable, servicename, tagFilterItems, topLevelOperationsRoute],
|
||||
);
|
||||
|
||||
const isQueryEnabled =
|
||||
|
||||
@@ -19,8 +19,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../../../constants/features';
|
||||
import { useAppContext } from '../../../../providers/App/App';
|
||||
import { IServiceName } from '../types';
|
||||
import { title } from './config';
|
||||
import ColumnWithLink from './TableRenderer/ColumnWithLink';
|
||||
@@ -44,11 +42,6 @@ function TopOperationMetrics(): JSX.Element {
|
||||
convertRawQueriesToTraceSelectedTags(queries) || [],
|
||||
);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const keyOperationWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
@@ -57,14 +50,13 @@ function TopOperationMetrics(): JSX.Element {
|
||||
promql: [],
|
||||
builder: topOperationQueries({
|
||||
servicename,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
},
|
||||
panelTypes: PANEL_TYPES.TABLE,
|
||||
}),
|
||||
[servicename, dotMetricsEnabled],
|
||||
[servicename],
|
||||
);
|
||||
|
||||
const updatedQuery = updateStepInterval(keyOperationWidget.query);
|
||||
|
||||
@@ -10,7 +10,6 @@ export interface IServiceName {
|
||||
|
||||
export interface TopOperationQueryFactoryProps {
|
||||
servicename: IServiceName['servicename'];
|
||||
dotMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface ExternalCallDurationByAddressProps extends ExternalCallProps {
|
||||
@@ -20,7 +19,6 @@ export interface ExternalCallDurationByAddressProps extends ExternalCallProps {
|
||||
export interface ExternalCallProps {
|
||||
servicename: IServiceName['servicename'];
|
||||
tagFilterItems: TagFilterItem[];
|
||||
dotMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface BuilderQueriesProps {
|
||||
@@ -52,7 +50,6 @@ export interface OperationPerSecProps {
|
||||
servicename: IServiceName['servicename'];
|
||||
tagFilterItems: TagFilterItem[];
|
||||
topLevelOperations: string[];
|
||||
dotMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface LatencyProps {
|
||||
@@ -60,7 +57,6 @@ export interface LatencyProps {
|
||||
tagFilterItems: TagFilterItem[];
|
||||
isSpanMetricEnable?: boolean;
|
||||
topLevelOperationsRoute: string[];
|
||||
dotMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface ApDexProps {
|
||||
@@ -78,5 +74,4 @@ export interface TableRendererProps {
|
||||
export interface ApDexMetricsQueryBuilderQueriesProps extends ApDexProps {
|
||||
delta: boolean;
|
||||
metricsBuckets: number[];
|
||||
dotMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
@@ -85,14 +85,11 @@ export enum WidgetKeys {
|
||||
HasError = 'hasError',
|
||||
Address = 'address',
|
||||
DurationNano = 'durationNano',
|
||||
StatusCodeNorm = 'status_code',
|
||||
StatusCode = 'status.code',
|
||||
Operation = 'operation',
|
||||
OperationName = 'operationName',
|
||||
Service_name_norm = 'service_name',
|
||||
Service_name = 'service.name',
|
||||
OTelServiceName = 'service.name',
|
||||
ServiceName = 'serviceName',
|
||||
SignozLatencyCountNorm = 'signoz_latency_count',
|
||||
SignozLatencyCount = 'signoz_latency.count',
|
||||
SignozDBLatencyCount = 'signoz_db_latency_count',
|
||||
DatabaseCallCount = 'signoz_database_call_count',
|
||||
@@ -101,10 +98,8 @@ export enum WidgetKeys {
|
||||
SignozCallsTotal = 'signoz_calls_total',
|
||||
SignozExternalCallLatencyCount = 'signoz_external_call_latency_count',
|
||||
SignozExternalCallLatencySum = 'signoz_external_call_latency_sum',
|
||||
Signoz_latency_bucket_norm = 'signoz_latency_bucket',
|
||||
Signoz_latency_bucket = 'signoz_latency.bucket',
|
||||
Db_system = 'db.system',
|
||||
Db_system_norm = 'db_system',
|
||||
SignozLatencyBucket = 'signoz_latency.bucket',
|
||||
DbSystem = 'db.system',
|
||||
}
|
||||
|
||||
export const topOperationMetricsDownloadOptions: DownloadOptions = {
|
||||
|
||||
@@ -32,5 +32,4 @@ export interface DatabaseCallsRPSProps extends DatabaseCallProps {
|
||||
export interface DatabaseCallProps {
|
||||
servicename: IServiceName['servicename'];
|
||||
tagFilterItems: TagFilterItem[];
|
||||
dotMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
||||
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { saveRecentQueryByExpression } from 'lib/recentQueries/saveRecentQuery';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { MetricsSearchProps } from './types';
|
||||
@@ -23,12 +24,14 @@ function MetricsSearch({
|
||||
);
|
||||
|
||||
const handleStageAndRunQuery = useCallback(() => {
|
||||
saveRecentQueryByExpression(DataSource.METRICS, currentQueryFilterExpression);
|
||||
onChange(currentQueryFilterExpression);
|
||||
onRunQuery?.();
|
||||
}, [currentQueryFilterExpression, onChange, onRunQuery]);
|
||||
|
||||
const handleRunQuery = useCallback(
|
||||
(expression: string): void => {
|
||||
saveRecentQueryByExpression(DataSource.METRICS, expression);
|
||||
setCurrentQueryFilterExpression(expression);
|
||||
onChange(expression);
|
||||
},
|
||||
|
||||
@@ -387,4 +387,42 @@ describe('useOptionsMenu', () => {
|
||||
expect(remaining).toHaveLength(seedColumns.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fieldsSelector.value drops legacy columns without a name', () => {
|
||||
it('excludes entries missing name while keeping valid columns', () => {
|
||||
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
|
||||
data: { data: { data: { keys: {} } } },
|
||||
isFetching: false,
|
||||
});
|
||||
(usePreferenceContext as jest.Mock).mockReturnValue({
|
||||
traces: {
|
||||
preferences: {
|
||||
columns: [
|
||||
{ name: 'body', fieldContext: 'log' },
|
||||
{ key: 'legacy-key-no-name', fieldContext: 'log' },
|
||||
{ name: 'timestamp', fieldContext: 'log' },
|
||||
],
|
||||
formatting: { format: 'table', maxLines: 1, fontSize: 'small' },
|
||||
},
|
||||
updateColumns: mockUpdateColumns,
|
||||
updateFormatting: mockUpdateFormatting,
|
||||
},
|
||||
logs: {
|
||||
preferences: { columns: [], formatting: {} },
|
||||
updateColumns: mockUpdateColumns,
|
||||
updateFormatting: mockUpdateFormatting,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useOptionsMenu({
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'count',
|
||||
}),
|
||||
);
|
||||
|
||||
const fields = result.current.config.fieldsSelector?.value ?? [];
|
||||
expect(fields.map((f) => f.name)).toStrictEqual(['body', 'timestamp']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -399,7 +399,7 @@ const useOptionsMenu = ({
|
||||
onReorder: reorderSelectColumns,
|
||||
},
|
||||
fieldsSelector: {
|
||||
value: preferences?.columns ?? [],
|
||||
value: preferences?.columns?.filter((item) => has(item, 'name')) ?? [],
|
||||
onFieldsChange: updateColumns,
|
||||
},
|
||||
format: {
|
||||
|
||||
@@ -53,8 +53,6 @@ import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../../../constants/features';
|
||||
import { useAppContext } from '../../../../providers/App/App';
|
||||
import { selectStyle } from './config';
|
||||
import { PLACEHOLDER } from './constant';
|
||||
import ExampleQueriesRendererForLogs from './ExampleQueriesRendererForLogs';
|
||||
@@ -104,11 +102,6 @@ function QueryBuilderSearch({
|
||||
|
||||
const [isEditingTag, setIsEditingTag] = useState(false);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const {
|
||||
updateTag,
|
||||
handleClearTag,
|
||||
@@ -128,7 +121,6 @@ function QueryBuilderSearch({
|
||||
exampleQueries,
|
||||
} = useAutoComplete(
|
||||
query,
|
||||
dotMetricsEnabled,
|
||||
whereClauseConfig,
|
||||
isLogsExplorerPage,
|
||||
isInfraMonitoring,
|
||||
@@ -146,7 +138,6 @@ function QueryBuilderSearch({
|
||||
const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues(
|
||||
searchValue,
|
||||
query,
|
||||
dotMetricsEnabled,
|
||||
searchKey,
|
||||
isLogsExplorerPage,
|
||||
isInfraMonitoring,
|
||||
|
||||
@@ -33,7 +33,7 @@ jest.mock('hooks/useNotifications', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const RESET_PASSWORD_ENDPOINT = '*/resetPassword';
|
||||
const RESET_PASSWORD_ENDPOINT = '*/api/v2/factor_password/reset';
|
||||
|
||||
const mockHistoryPush = history.push as jest.MockedFunction<
|
||||
typeof history.push
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-use';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
import { Form, Input as AntdInput } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import resetPasswordApi from 'api/v1/factor_password/resetPassword';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { useResetPassword } from 'api/generated/services/users';
|
||||
import AuthError from 'components/AuthError/AuthError';
|
||||
import AuthPageContainer from 'components/AuthPageContainer';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -14,7 +15,6 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { ArrowRight, CircleAlert, KeyRound } from '@signozhq/icons';
|
||||
import { Label } from 'pages/SignUp/styles';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { FormContainer } from './styles';
|
||||
|
||||
@@ -26,40 +26,41 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
const [confirmPasswordError, setConfirmPasswordError] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<APIError | null>();
|
||||
|
||||
const [isValidPassword, setIsValidPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation(['common']);
|
||||
const { search } = useLocation();
|
||||
const params = new URLSearchParams(search);
|
||||
const token = params.get('token');
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const {
|
||||
mutate: resetPassword,
|
||||
isLoading,
|
||||
error: mutationError,
|
||||
} = useResetPassword();
|
||||
|
||||
const errorMessage = useMemo(
|
||||
() => convertToApiError(mutationError),
|
||||
[mutationError],
|
||||
);
|
||||
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const handleFormSubmit: () => Promise<void> = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setErrorMessage(null);
|
||||
const { password } = form.getFieldsValue();
|
||||
const handleFormSubmit = (): void => {
|
||||
const { password } = form.getFieldsValue();
|
||||
|
||||
await resetPasswordApi({
|
||||
password,
|
||||
token: token || '',
|
||||
});
|
||||
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
history.push(ROUTES.LOGIN);
|
||||
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setErrorMessage(error as APIError);
|
||||
}
|
||||
resetPassword(
|
||||
{ data: { password, token: token || '' } },
|
||||
{
|
||||
onSuccess: (): void => {
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
history.push(ROUTES.LOGIN);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const validatePassword = (): boolean => {
|
||||
@@ -222,7 +223,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
color="primary"
|
||||
type="submit"
|
||||
data-attr="reset-password"
|
||||
disabled={!isValidPassword || loading}
|
||||
disabled={!isValidPassword || isLoading}
|
||||
className="reset-password-submit-button"
|
||||
suffix={<ArrowRight size={16} />}
|
||||
>
|
||||
|
||||
@@ -14,8 +14,6 @@ import { SelectOption } from 'types/common/select';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../constants/features';
|
||||
import { useAppContext } from '../../providers/App/App';
|
||||
import QueryChip from './components/QueryChip';
|
||||
import { QueryChipItem, SearchContainer } from './styles';
|
||||
|
||||
@@ -42,12 +40,7 @@ function ResourceAttributesFilter({
|
||||
SelectOption<string, string>[]
|
||||
>([]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const resourceDeploymentKey = getResourceDeploymentKeys(dotMetricsEnabled);
|
||||
const resourceDeploymentKey = getResourceDeploymentKeys();
|
||||
|
||||
const [selectedEnvironments, setSelectedEnvironments] = useState<string[]>([]);
|
||||
|
||||
@@ -73,21 +66,20 @@ function ResourceAttributesFilter({
|
||||
}, [queries, resourceDeploymentKey]);
|
||||
|
||||
useEffect(() => {
|
||||
getEnvironmentTagKeys(dotMetricsEnabled).then((tagKeys) => {
|
||||
getEnvironmentTagKeys().then((tagKeys) => {
|
||||
if (tagKeys && Array.isArray(tagKeys) && tagKeys.length > 0) {
|
||||
getEnvironmentTagValues(dotMetricsEnabled).then((tagValues) => {
|
||||
getEnvironmentTagValues().then((tagValues) => {
|
||||
setEnvironments(tagValues);
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [dotMetricsEnabled]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="resourceAttributesFilter-container">
|
||||
<div className="environment-selector">
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
key={selectedEnvironments.join('')}
|
||||
showSearch
|
||||
mode="multiple"
|
||||
value={selectedEnvironments}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { createMemoryHistory, MemoryHistory } from 'history';
|
||||
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
||||
import { IResourceAttribute } from 'hooks/useResourceAttribute/types';
|
||||
import { encode } from 'js-base64';
|
||||
|
||||
import ResourceAttributesFilter from '../ResourceAttributesFilter';
|
||||
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
push: jest.fn(),
|
||||
location: { search: '', pathname: '/' },
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('api/metrics/getResourceAttributes', () => ({
|
||||
getResourceAttributesTagKeys: jest.fn(),
|
||||
getResourceAttributesTagValues: jest.fn(),
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line import/first, import/order
|
||||
import {
|
||||
getResourceAttributesTagKeys,
|
||||
getResourceAttributesTagValues,
|
||||
// eslint-disable-next-line import/newline-after-import
|
||||
} from 'api/metrics/getResourceAttributes';
|
||||
// eslint-disable-next-line import/first, import/order
|
||||
import history from 'lib/history';
|
||||
|
||||
const mockTagKeys = getResourceAttributesTagKeys as jest.MockedFunction<
|
||||
typeof getResourceAttributesTagKeys
|
||||
>;
|
||||
const mockTagValues = getResourceAttributesTagValues as jest.MockedFunction<
|
||||
typeof getResourceAttributesTagValues
|
||||
>;
|
||||
|
||||
function tagKeysPayload(keys: string[]): never {
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'ok',
|
||||
payload: {
|
||||
data: {
|
||||
attributeKeys: keys.map((key) => ({
|
||||
key,
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
})),
|
||||
},
|
||||
},
|
||||
} as unknown as never;
|
||||
}
|
||||
|
||||
function tagValuesPayload(values: string[]): never {
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'ok',
|
||||
payload: { data: { stringAttributeValues: values } },
|
||||
} as unknown as never;
|
||||
}
|
||||
|
||||
function seedUrl(queries: IResourceAttribute[], pathname: string): void {
|
||||
const location = history.location as { search: string; pathname: string };
|
||||
location.search = queries.length
|
||||
? `?resourceAttribute=${encode(JSON.stringify(queries))}`
|
||||
: '';
|
||||
location.pathname = pathname;
|
||||
}
|
||||
|
||||
function renderFilter(pathname: string): MemoryHistory {
|
||||
const routerHistory = createMemoryHistory({
|
||||
initialEntries: [`${pathname}${history.location.search}`],
|
||||
});
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
function Wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Router history={routerHistory}>
|
||||
<ResourceProvider>{children}</ResourceProvider>
|
||||
</Router>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<ResourceAttributesFilter />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
return routerHistory;
|
||||
}
|
||||
|
||||
describe('ResourceAttributesFilter', () => {
|
||||
beforeEach(() => {
|
||||
mockTagKeys.mockReset();
|
||||
mockTagValues.mockReset();
|
||||
mockTagKeys.mockResolvedValue(
|
||||
tagKeysPayload(['resource_deployment.environment']),
|
||||
);
|
||||
mockTagValues.mockResolvedValue(tagValuesPayload(['production', 'staging']));
|
||||
seedUrl([], '/');
|
||||
});
|
||||
|
||||
it('shows every applied filter on the service map, including ones it cannot apply', async () => {
|
||||
seedUrl(
|
||||
[
|
||||
{
|
||||
id: 'svc',
|
||||
tagKey: 'resource_service_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['frontend'],
|
||||
},
|
||||
{
|
||||
id: 'env',
|
||||
tagKey: 'resource_deployment.environment',
|
||||
operator: 'IN',
|
||||
tagValue: ['production'],
|
||||
},
|
||||
],
|
||||
ROUTES.SERVICE_MAP,
|
||||
);
|
||||
|
||||
renderFilter(ROUTES.SERVICE_MAP);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/service\.name/)).toBeInTheDocument(),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('resource-environment-filter')
|
||||
.querySelector('.ant-select-selection-item'),
|
||||
).toHaveTextContent('production'),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the environment dropdown open so more than one environment can be picked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderFilter('/services');
|
||||
|
||||
const environmentFilter = screen.getByTestId('resource-environment-filter');
|
||||
await user.click(
|
||||
environmentFilter.querySelector('input') as HTMLInputElement,
|
||||
);
|
||||
|
||||
await user.click(await screen.findByTitle('production'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByTitle('staging').closest('.ant-select-dropdown'),
|
||||
).not.toHaveClass('ant-select-dropdown-hidden'),
|
||||
);
|
||||
|
||||
await user.click(screen.getByTitle('staging'));
|
||||
|
||||
await waitFor(() => {
|
||||
const selected = Array.from(
|
||||
environmentFilter.querySelectorAll('.ant-select-selection-item-content'),
|
||||
).map((node) => node.textContent);
|
||||
expect(selected).toStrictEqual(['production', 'staging']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,8 +3,6 @@ import {
|
||||
getResourceDeploymentKeys,
|
||||
} from 'hooks/useResourceAttribute/utils';
|
||||
|
||||
import { FeatureKeys } from '../../../../constants/features';
|
||||
import { useAppContext } from '../../../../providers/App/App';
|
||||
import { QueryChipContainer, QueryChipItem } from '../../styles';
|
||||
import { IQueryChipProps } from './types';
|
||||
|
||||
@@ -13,13 +11,7 @@ function QueryChip({ queryData, onClose }: IQueryChipProps): JSX.Element {
|
||||
onClose(queryData.id);
|
||||
};
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const isClosable =
|
||||
queryData.tagKey !== getResourceDeploymentKeys(dotMetricsEnabled);
|
||||
const isClosable = queryData.tagKey !== getResourceDeploymentKeys();
|
||||
|
||||
return (
|
||||
<QueryChipContainer>
|
||||
|
||||
@@ -4,8 +4,6 @@ import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import { ServiceMetricsProps } from '../types';
|
||||
import { getQueryRangeRequestData } from '../utils';
|
||||
import ServiceMetricTable from './ServiceMetricTable';
|
||||
@@ -18,19 +16,13 @@ function ServiceMetricsApplication({
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const queryRangeRequestData = useMemo(
|
||||
() =>
|
||||
getQueryRangeRequestData({
|
||||
topLevelOperations,
|
||||
globalSelectedInterval,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
[globalSelectedInterval, topLevelOperations, dotMetricsEnabled],
|
||||
[globalSelectedInterval, topLevelOperations],
|
||||
);
|
||||
return (
|
||||
<ServiceMetricTable
|
||||
|
||||
@@ -19,13 +19,10 @@ import {
|
||||
|
||||
export const serviceMetricsQuery = (
|
||||
topLevelOperation: [keyof ServiceDataProps, string[]],
|
||||
dotMetricsEnabled: boolean,
|
||||
): QueryBuilderData => {
|
||||
const p99AutoCompleteData: BaseAutocompleteData = {
|
||||
dataType: DataTypes.Float64,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Signoz_latency_bucket
|
||||
: WidgetKeys.Signoz_latency_bucket_norm,
|
||||
key: WidgetKeys.SignozLatencyBucket,
|
||||
type: '',
|
||||
};
|
||||
|
||||
@@ -53,9 +50,7 @@ export const serviceMetricsQuery = (
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -78,9 +73,7 @@ export const serviceMetricsQuery = (
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -90,7 +83,7 @@ export const serviceMetricsQuery = (
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.Int64,
|
||||
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
|
||||
key: WidgetKeys.StatusCode,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -113,9 +106,7 @@ export const serviceMetricsQuery = (
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -138,9 +129,7 @@ export const serviceMetricsQuery = (
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -193,9 +182,7 @@ export const serviceMetricsQuery = (
|
||||
const groupBy: BaseAutocompleteData[] = [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -17,8 +17,6 @@ import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { Tags } from 'types/reducer/trace';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import SkipOnBoardingModal from '../SkipOnBoardModal';
|
||||
import ServiceTraceTable from './ServiceTracesTable';
|
||||
|
||||
@@ -40,11 +38,6 @@ function ServiceTraces(): JSX.Element {
|
||||
selectedTags,
|
||||
});
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
useErrorNotification(error);
|
||||
|
||||
const services = data || [];
|
||||
@@ -62,7 +55,7 @@ function ServiceTraces(): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && !isUndefined(data)) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(),
|
||||
)?.tagValue;
|
||||
|
||||
const rps = data.reduce((total, service) => total + service.callRate, 0);
|
||||
|
||||
@@ -26,7 +26,6 @@ export interface ServiceMetricsTableProps {
|
||||
export interface GetQueryRangeRequestDataProps {
|
||||
topLevelOperations: [keyof ServiceDataProps, string[]][];
|
||||
globalSelectedInterval: Time | CustomTimeType;
|
||||
dotMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface GetServiceListFromQueryProps {
|
||||
|
||||
@@ -26,7 +26,6 @@ export function getSeriesValue(
|
||||
export const getQueryRangeRequestData = ({
|
||||
topLevelOperations,
|
||||
globalSelectedInterval,
|
||||
dotMetricsEnabled,
|
||||
}: GetQueryRangeRequestDataProps): GetQueryResultsProps[] => {
|
||||
const requestData: GetQueryResultsProps[] = [];
|
||||
topLevelOperations.forEach((operation) => {
|
||||
@@ -34,7 +33,7 @@ export const getQueryRangeRequestData = ({
|
||||
query: {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
promql: [],
|
||||
builder: serviceMetricsQuery(operation, dotMetricsEnabled),
|
||||
builder: serviceMetricsQuery(operation),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
},
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import type { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type {
|
||||
AuthtypesGettableRoleDTO,
|
||||
AuthtypesUserRoleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
getGetRolesByUserIDQueryKey,
|
||||
useGetRolesByUserID,
|
||||
useRemoveUserRoleByUserIDAndRoleID,
|
||||
useSetRoleByUserID,
|
||||
useCreateUserRole,
|
||||
useDeleteUserRole,
|
||||
useGetUser,
|
||||
} from 'api/generated/services/users';
|
||||
import { retryOn429 } from 'utils/errorUtils';
|
||||
|
||||
const enum PromiseStatus {
|
||||
Fulfilled = 'fulfilled',
|
||||
Rejected = 'rejected',
|
||||
}
|
||||
|
||||
// Stable identity so the memos below do not recompute on every render.
|
||||
const EMPTY_USER_ROLES: AuthtypesUserRoleDTO[] = [];
|
||||
|
||||
export interface MemberRoleUpdateFailure {
|
||||
roleName: string;
|
||||
error: unknown;
|
||||
@@ -33,31 +36,31 @@ export function useMemberRoleManager(
|
||||
userId: string,
|
||||
enabled: boolean,
|
||||
): UseMemberRoleManagerResult {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading } = useGetRolesByUserID(
|
||||
const { data, isLoading } = useGetUser(
|
||||
{ id: userId },
|
||||
{ query: { enabled: !!userId && enabled } },
|
||||
);
|
||||
|
||||
const userRoles = data?.data?.userRoles ?? EMPTY_USER_ROLES;
|
||||
|
||||
const currentRoles = useMemo<AuthtypesGettableRoleDTO[]>(
|
||||
() => data?.data ?? [],
|
||||
[data?.data],
|
||||
() => userRoles.map((userRole) => userRole.role),
|
||||
[userRoles],
|
||||
);
|
||||
|
||||
const { mutateAsync: setRole } = useSetRoleByUserID({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
const { mutateAsync: removeRole } = useRemoveUserRoleByUserIDAndRoleID({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
|
||||
const invalidateRoles = useCallback(
|
||||
() =>
|
||||
queryClient.invalidateQueries(getGetRolesByUserIDQueryKey({ id: userId })),
|
||||
[userId, queryClient],
|
||||
// DELETE /api/v2/user_roles/{id} is keyed by the user_role join row, not the role.
|
||||
const assignmentIdByRoleId = useMemo(
|
||||
() => new Map(userRoles.map((userRole) => [userRole.roleId, userRole.id])),
|
||||
[userRoles],
|
||||
);
|
||||
|
||||
const { mutateAsync: createUserRole } = useCreateUserRole({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
const { mutateAsync: deleteUserRole } = useDeleteUserRole({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
|
||||
const applyDiff = useCallback(
|
||||
async (
|
||||
localRoleIds: string[],
|
||||
@@ -80,30 +83,33 @@ export function useMemberRoleManager(
|
||||
const allOperations = [
|
||||
...addedRoles.map((role) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof setRole> =>
|
||||
setRole({
|
||||
pathParams: { id: userId },
|
||||
data: { name: role.name ?? '' },
|
||||
}),
|
||||
})),
|
||||
...removedRoles.map((role) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof removeRole> =>
|
||||
removeRole({ pathParams: { id: userId, roleId: role.id ?? '' } }),
|
||||
run: (): ReturnType<typeof createUserRole> =>
|
||||
createUserRole({ data: { userId, roleId: role.id ?? '' } }),
|
||||
})),
|
||||
...removedRoles
|
||||
.map((role) => ({
|
||||
role,
|
||||
assignmentId: assignmentIdByRoleId.get(role.id ?? ''),
|
||||
}))
|
||||
.filter(
|
||||
(
|
||||
entry,
|
||||
): entry is {
|
||||
role: AuthtypesGettableRoleDTO;
|
||||
assignmentId: string;
|
||||
} => !!entry.assignmentId,
|
||||
)
|
||||
.map(({ role, assignmentId }) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof deleteUserRole> =>
|
||||
deleteUserRole({ pathParams: { id: assignmentId } }),
|
||||
})),
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
allOperations.map((op) => op.run()),
|
||||
);
|
||||
|
||||
const successCount = results.filter(
|
||||
(r) => r.status === PromiseStatus.Fulfilled,
|
||||
).length;
|
||||
if (successCount > 0) {
|
||||
await invalidateRoles();
|
||||
}
|
||||
|
||||
const failures: MemberRoleUpdateFailure[] = [];
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === PromiseStatus.Rejected) {
|
||||
@@ -113,7 +119,6 @@ export function useMemberRoleManager(
|
||||
error: result.reason,
|
||||
onRetry: async (): Promise<void> => {
|
||||
await run();
|
||||
await invalidateRoles();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -121,7 +126,7 @@ export function useMemberRoleManager(
|
||||
|
||||
return failures;
|
||||
},
|
||||
[userId, currentRoles, setRole, removeRole, invalidateRoles],
|
||||
[userId, currentRoles, assignmentIdByRoleId, createUserRole, deleteUserRole],
|
||||
);
|
||||
|
||||
return { currentRoles, isLoading, applyDiff };
|
||||
|
||||
@@ -27,7 +27,6 @@ export type WhereClauseConfig = {
|
||||
|
||||
export const useAutoComplete = (
|
||||
query: IBuilderQuery,
|
||||
dotMetricsEnabled: boolean,
|
||||
whereClauseConfig?: WhereClauseConfig,
|
||||
shouldUseSuggestions?: boolean,
|
||||
isInfraMonitoring?: boolean,
|
||||
@@ -40,7 +39,6 @@ export const useAutoComplete = (
|
||||
const { keys, results, isFetching, exampleQueries } = useFetchKeysAndValues(
|
||||
searchValue,
|
||||
query,
|
||||
dotMetricsEnabled,
|
||||
searchKey,
|
||||
shouldUseSuggestions,
|
||||
isInfraMonitoring,
|
||||
|
||||
@@ -48,7 +48,6 @@ type IuseFetchKeysAndValues = {
|
||||
export const useFetchKeysAndValues = (
|
||||
searchValue: string,
|
||||
query: IBuilderQuery,
|
||||
dotMetricsEnabled: boolean,
|
||||
searchKey: string,
|
||||
shouldUseSuggestions?: boolean,
|
||||
isInfraMonitoring?: boolean,
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { encode } from 'js-base64';
|
||||
|
||||
import { FeatureKeys } from '../../constants/features';
|
||||
import { useAppContext } from '../../providers/App/App';
|
||||
import { whilelistedKeys } from './config';
|
||||
import { ResourceContext } from './context';
|
||||
import {
|
||||
IResourceAttribute,
|
||||
@@ -58,11 +54,6 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const dispatchQueries = useCallback(
|
||||
(queries: IResourceAttribute[]): void => {
|
||||
urlQuery.set(
|
||||
@@ -78,7 +69,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
||||
|
||||
const loadTagKeys = (): void => {
|
||||
handleLoading(true);
|
||||
GetTagKeys(dotMetricsEnabled)
|
||||
GetTagKeys()
|
||||
.then((tagKeys) => {
|
||||
const options = mappingWithRoutesAndKeys(pathname, tagKeys);
|
||||
setOptionsData({ options, mode: undefined });
|
||||
@@ -161,15 +152,15 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
||||
|
||||
setSelectedQueries([...value]);
|
||||
},
|
||||
[optionsData.mode, step, staging, dotMetricsEnabled, pathname],
|
||||
[optionsData.mode, step, staging, pathname],
|
||||
);
|
||||
|
||||
const handleEnvironmentChange = useCallback(
|
||||
(environments: string[]): void => {
|
||||
const staging = [getResourceDeploymentKeys(dotMetricsEnabled), 'IN'];
|
||||
const staging = [getResourceDeploymentKeys(), 'IN'];
|
||||
|
||||
const queriesCopy = queries.filter(
|
||||
(query) => query.tagKey !== getResourceDeploymentKeys(dotMetricsEnabled),
|
||||
(query) => query.tagKey !== getResourceDeploymentKeys(),
|
||||
);
|
||||
|
||||
if (environments && Array.isArray(environments) && environments.length > 0) {
|
||||
@@ -184,7 +175,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
||||
|
||||
setStep('Idle');
|
||||
},
|
||||
[dispatchQueries, dotMetricsEnabled, queries],
|
||||
[dispatchQueries, queries],
|
||||
);
|
||||
|
||||
const handleClose = useCallback(
|
||||
@@ -202,16 +193,9 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
||||
setOptionsData({ mode: undefined, options: [] });
|
||||
}, [dispatchQueries]);
|
||||
|
||||
const getVisibleQueries = useMemo(() => {
|
||||
if (pathname === ROUTES.SERVICE_MAP) {
|
||||
return queries.filter((query) => whilelistedKeys.includes(query.tagKey));
|
||||
}
|
||||
return queries;
|
||||
}, [queries, pathname]);
|
||||
|
||||
const value: IResourceAttributeProps = useMemo(
|
||||
() => ({
|
||||
queries: getVisibleQueries,
|
||||
queries,
|
||||
staging,
|
||||
handleClearAll,
|
||||
handleClose,
|
||||
@@ -234,7 +218,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
||||
staging,
|
||||
selectedQuery,
|
||||
optionsData,
|
||||
getVisibleQueries,
|
||||
queries,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -2,13 +2,9 @@ import { ReactNode } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { createMemoryHistory, MemoryHistory } from 'history';
|
||||
import { encode } from 'js-base64';
|
||||
import { AppContext } from 'providers/App/App';
|
||||
import { IAppContext } from 'providers/App/types';
|
||||
import { getAppContextMock } from 'tests/test-utils';
|
||||
|
||||
import ResourceProvider from '../ResourceProvider';
|
||||
import useResourceAttribute from '../useResourceAttribute';
|
||||
@@ -55,10 +51,8 @@ const mockTagValues = getResourceAttributesTagValues as jest.MockedFunction<
|
||||
|
||||
function createWrapper({
|
||||
routerHistory,
|
||||
appContextOverrides,
|
||||
}: {
|
||||
routerHistory: MemoryHistory;
|
||||
appContextOverrides?: Partial<IAppContext>;
|
||||
}): ({ children }: { children: ReactNode }) => JSX.Element {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
@@ -66,13 +60,9 @@ function createWrapper({
|
||||
return function Wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppContext.Provider
|
||||
value={getAppContextMock('ADMIN', appContextOverrides)}
|
||||
>
|
||||
<Router history={routerHistory}>
|
||||
<ResourceProvider>{children}</ResourceProvider>
|
||||
</Router>
|
||||
</AppContext.Provider>
|
||||
<Router history={routerHistory}>
|
||||
<ResourceProvider>{children}</ResourceProvider>
|
||||
</Router>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
@@ -411,7 +401,7 @@ describe('ResourceProvider', () => {
|
||||
});
|
||||
|
||||
describe('handleEnvironmentChange', () => {
|
||||
it('adds an environment query when envs are provided', async () => {
|
||||
it('adds a dotted environment query when envs are provided', async () => {
|
||||
const routerHistory = createMemoryHistory({ initialEntries: ['/'] });
|
||||
const { result } = renderHook(() => useResourceAttribute(), {
|
||||
wrapper: createWrapper({ routerHistory }),
|
||||
@@ -424,7 +414,7 @@ describe('ResourceProvider', () => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.queries).toHaveLength(1);
|
||||
expect(result.current.queries[0]).toMatchObject({
|
||||
tagKey: 'resource_deployment_environment',
|
||||
tagKey: 'resource_deployment.environment',
|
||||
operator: 'IN',
|
||||
tagValue: ['production'],
|
||||
});
|
||||
@@ -435,7 +425,7 @@ describe('ResourceProvider', () => {
|
||||
const seeded = [
|
||||
{
|
||||
id: 'env',
|
||||
tagKey: 'resource_deployment_environment',
|
||||
tagKey: 'resource_deployment.environment',
|
||||
operator: 'IN',
|
||||
tagValue: ['production'],
|
||||
},
|
||||
@@ -459,7 +449,7 @@ describe('ResourceProvider', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const tagKeys = result.current.queries.map((q) => q.tagKey);
|
||||
expect(tagKeys).not.toContain('resource_deployment_environment');
|
||||
expect(tagKeys).not.toContain('resource_deployment.environment');
|
||||
expect(tagKeys).toContain('resource_service_name');
|
||||
});
|
||||
});
|
||||
@@ -468,7 +458,7 @@ describe('ResourceProvider', () => {
|
||||
const seeded = [
|
||||
{
|
||||
id: 'env',
|
||||
tagKey: 'resource_deployment_environment',
|
||||
tagKey: 'resource_deployment.environment',
|
||||
operator: 'IN',
|
||||
tagValue: ['production'],
|
||||
},
|
||||
@@ -486,43 +476,13 @@ describe('ResourceProvider', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const envQueries = result.current.queries.filter(
|
||||
(q) => q.tagKey === 'resource_deployment_environment',
|
||||
(q) => q.tagKey === 'resource_deployment.environment',
|
||||
);
|
||||
expect(envQueries).toHaveLength(1);
|
||||
expect(envQueries[0].tagValue).toStrictEqual(['staging']);
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the dotted deployment env key when DOT_METRICS_ENABLED is active', async () => {
|
||||
const routerHistory = createMemoryHistory({ initialEntries: ['/'] });
|
||||
const { result } = renderHook(() => useResourceAttribute(), {
|
||||
wrapper: createWrapper({
|
||||
routerHistory,
|
||||
appContextOverrides: {
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.DOT_METRICS_ENABLED,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleEnvironmentChange(['production']);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.queries[0].tagKey).toBe(
|
||||
'resource_deployment.environment',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves unrelated query params when dispatching', async () => {
|
||||
const routerHistory = createMemoryHistory({
|
||||
initialEntries: ['/?tab=overview'],
|
||||
@@ -544,22 +504,23 @@ describe('ResourceProvider', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVisibleQueries (SERVICE_MAP filtering)', () => {
|
||||
it('filters queries down to whitelisted keys on SERVICE_MAP', () => {
|
||||
const seeded = [
|
||||
{
|
||||
id: 'a',
|
||||
tagKey: 'resource_service_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['frontend'],
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
tagKey: 'resource_k8s_cluster_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['prod'],
|
||||
},
|
||||
];
|
||||
describe('SERVICE_MAP', () => {
|
||||
const seeded = [
|
||||
{
|
||||
id: 'a',
|
||||
tagKey: 'resource_service_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['frontend'],
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
tagKey: 'resource_k8s_cluster_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['prod'],
|
||||
},
|
||||
];
|
||||
|
||||
it('exposes every query from the URL, including ones the map cannot apply', () => {
|
||||
mockLibHistory(
|
||||
`?resourceAttribute=${encode(JSON.stringify(seeded))}`,
|
||||
ROUTES.SERVICE_MAP,
|
||||
@@ -572,24 +533,10 @@ describe('ResourceProvider', () => {
|
||||
wrapper: createWrapper({ routerHistory }),
|
||||
});
|
||||
|
||||
expect(result.current.queries).toStrictEqual([seeded[1]]);
|
||||
expect(result.current.queries).toStrictEqual(seeded);
|
||||
});
|
||||
|
||||
it('returns all queries on non-SERVICE_MAP routes', () => {
|
||||
const seeded = [
|
||||
{
|
||||
id: 'a',
|
||||
tagKey: 'resource_service_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['frontend'],
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
tagKey: 'resource_k8s_cluster_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['prod'],
|
||||
},
|
||||
];
|
||||
mockLibHistory(
|
||||
`?resourceAttribute=${encode(JSON.stringify(seeded))}`,
|
||||
'/services',
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
import { whilelistedKeys } from '../config';
|
||||
import { mappingWithRoutesAndKeys } from '../utils';
|
||||
import {
|
||||
filterServiceMapSupportedQueries,
|
||||
mappingWithRoutesAndKeys,
|
||||
} from '../utils';
|
||||
|
||||
describe('useResourceAttribute config', () => {
|
||||
describe('whilelistedKeys', () => {
|
||||
it('should include underscore-notation keys (DOT_METRICS_ENABLED=false)', () => {
|
||||
it('should include underscore-notation keys', () => {
|
||||
expect(whilelistedKeys).toContain('resource_deployment_environment');
|
||||
expect(whilelistedKeys).toContain('resource_k8s_cluster_name');
|
||||
expect(whilelistedKeys).toContain('resource_k8s_cluster_namespace');
|
||||
});
|
||||
|
||||
it('should include dot-notation keys (DOT_METRICS_ENABLED=true)', () => {
|
||||
it('should include dot-notation keys', () => {
|
||||
expect(whilelistedKeys).toContain('resource_deployment.environment');
|
||||
expect(whilelistedKeys).toContain('resource_k8s.cluster.name');
|
||||
expect(whilelistedKeys).toContain('resource_k8s.cluster.namespace');
|
||||
@@ -74,4 +77,29 @@ describe('useResourceAttribute config', () => {
|
||||
expect(result).toStrictEqual(allFilters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterServiceMapSupportedQueries', () => {
|
||||
const environmentQuery = {
|
||||
id: 'env',
|
||||
tagKey: 'resource_deployment_environment',
|
||||
operator: 'IN',
|
||||
tagValue: ['production'],
|
||||
};
|
||||
const serviceQuery = {
|
||||
id: 'svc',
|
||||
tagKey: 'resource_service_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['frontend'],
|
||||
};
|
||||
|
||||
it('should keep only the queries the service map can filter on', () => {
|
||||
expect(
|
||||
filterServiceMapSupportedQueries([environmentQuery, serviceQuery]),
|
||||
).toStrictEqual([environmentQuery]);
|
||||
});
|
||||
|
||||
it('should return an empty list when no query is supported', () => {
|
||||
expect(filterServiceMapSupportedQueries([serviceQuery])).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,19 +144,11 @@ export const OperatorSchema: IOption[] = OperatorConversions.map(
|
||||
}),
|
||||
);
|
||||
|
||||
export const getResourceDeploymentKeys = (
|
||||
dotMetricsEnabled: boolean,
|
||||
): string => {
|
||||
if (dotMetricsEnabled) {
|
||||
return 'resource_deployment.environment';
|
||||
}
|
||||
return 'resource_deployment_environment';
|
||||
};
|
||||
export const getResourceDeploymentKeys = (): string =>
|
||||
'resource_deployment.environment';
|
||||
|
||||
export const GetTagKeys = async (
|
||||
dotMetricsEnabled: boolean,
|
||||
): Promise<IOption[]> => {
|
||||
const resourceDeploymentKey = getResourceDeploymentKeys(dotMetricsEnabled);
|
||||
export const GetTagKeys = async (): Promise<IOption[]> => {
|
||||
const resourceDeploymentKey = getResourceDeploymentKeys();
|
||||
const { payload } = await getResourceAttributesTagKeys({
|
||||
metricName: 'signoz_calls_total',
|
||||
match: 'resource_',
|
||||
@@ -176,12 +168,10 @@ export const GetTagKeys = async (
|
||||
}));
|
||||
};
|
||||
|
||||
export const getEnvironmentTagKeys = async (
|
||||
dotMetricsEnabled: boolean,
|
||||
): Promise<IOption[]> => {
|
||||
export const getEnvironmentTagKeys = async (): Promise<IOption[]> => {
|
||||
const { payload } = await getResourceAttributesTagKeys({
|
||||
metricName: 'signoz_calls_total',
|
||||
match: getResourceDeploymentKeys(dotMetricsEnabled),
|
||||
match: getResourceDeploymentKeys(),
|
||||
});
|
||||
if (!payload || !payload?.data) {
|
||||
return [];
|
||||
@@ -194,11 +184,9 @@ export const getEnvironmentTagKeys = async (
|
||||
}));
|
||||
};
|
||||
|
||||
export const getEnvironmentTagValues = async (
|
||||
dotMetricsEnabled: boolean,
|
||||
): Promise<IOption[]> => {
|
||||
export const getEnvironmentTagValues = async (): Promise<IOption[]> => {
|
||||
const { payload } = await getResourceAttributesTagValues({
|
||||
tagKey: getResourceDeploymentKeys(dotMetricsEnabled),
|
||||
tagKey: getResourceDeploymentKeys(),
|
||||
metricName: 'signoz_calls_total',
|
||||
});
|
||||
|
||||
@@ -293,3 +281,8 @@ export const mappingWithRoutesAndKeys = (
|
||||
}
|
||||
return filters;
|
||||
};
|
||||
|
||||
export const filterServiceMapSupportedQueries = (
|
||||
queries: IResourceAttribute[],
|
||||
): IResourceAttribute[] =>
|
||||
queries.filter((query) => whilelistedKeys.includes(query.tagKey));
|
||||
|
||||
55
frontend/src/lib/logs/flatLogData.test.ts
Normal file
55
frontend/src/lib/logs/flatLogData.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { getLogFieldValue } from './flatLogData';
|
||||
|
||||
const asLog = (partial: Partial<ILog>): ILog => partial as unknown as ILog;
|
||||
|
||||
describe('getLogFieldValue', () => {
|
||||
it('resolves a nested body field by dotted key when use_json_body is on', () => {
|
||||
const log = asLog({ body: { a: { b: { c: 'deep' } } } });
|
||||
expect(getLogFieldValue(log, 'a.b.c', true)).toBe('deep');
|
||||
});
|
||||
|
||||
it('ignores body when use_json_body is off', () => {
|
||||
const log = asLog({ body: { a: { b: { c: 'deep' } } } });
|
||||
expect(getLogFieldValue(log, 'a.b.c', false)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ignores a stringified body even when use_json_body is on', () => {
|
||||
const log = asLog({ body: '{"a":{"b":1}}' });
|
||||
expect(getLogFieldValue(log, 'a.b', true)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('prefers the body value over attributes when the key exists in both (body first)', () => {
|
||||
const log = asLog({
|
||||
attributes_string: { 'a.b': 'attr' } as never,
|
||||
body: { a: { b: 'bodyval' } },
|
||||
});
|
||||
expect(getLogFieldValue(log, 'a.b', true)).toBe('bodyval');
|
||||
});
|
||||
|
||||
it('falls back to attributes when the key is not in the body', () => {
|
||||
const log = asLog({
|
||||
attributes_string: { 'x.y': 'attr' } as never,
|
||||
body: { other: 1 },
|
||||
});
|
||||
expect(getLogFieldValue(log, 'x.y', true)).toBe('attr');
|
||||
});
|
||||
|
||||
it('preserves falsy body values (0, false, empty string)', () => {
|
||||
const log = asLog({ body: { n: 0, flag: false, s: '' } });
|
||||
expect(getLogFieldValue(log, 'n', true)).toBe(0);
|
||||
expect(getLogFieldValue(log, 'flag', true)).toBe(false);
|
||||
expect(getLogFieldValue(log, 's', true)).toBe('');
|
||||
});
|
||||
|
||||
it('returns undefined when the body path is missing', () => {
|
||||
const log = asLog({ body: { x: 1 } });
|
||||
expect(getLogFieldValue(log, 'nope', true)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined when a mid path segment is not an object', () => {
|
||||
const log = asLog({ body: { a: { b: 'leaf' } } });
|
||||
expect(getLogFieldValue(log, 'a.b.c', true)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { ILog, ILogBody } from 'types/api/logs/log';
|
||||
|
||||
export function FlatLogData(log: ILog): Record<string, string> {
|
||||
const flattenLogObject: Record<string, string> = {};
|
||||
@@ -15,3 +15,29 @@ export function FlatLogData(log: ILog): Record<string, string> {
|
||||
});
|
||||
return flattenLogObject;
|
||||
}
|
||||
|
||||
function getBodyFieldValue(body: ILogBody, key: string): unknown {
|
||||
return key.split('.').reduce<unknown>((acc, segment) => {
|
||||
if (acc && typeof acc === 'object' && !Array.isArray(acc)) {
|
||||
return (acc as Record<string, unknown>)[segment];
|
||||
}
|
||||
return undefined;
|
||||
}, body);
|
||||
}
|
||||
|
||||
// Resolve one field for the logs table. A JSON body is checked first (use_json_body
|
||||
// only), splitting the key on `.`; otherwise fall back to FlatLogData
|
||||
// (attributes/resources/scope/top-level).
|
||||
export function getLogFieldValue(
|
||||
log: ILog,
|
||||
fieldName: string,
|
||||
isBodyJsonEnabled: boolean,
|
||||
): unknown {
|
||||
if (isBodyJsonEnabled && log.body && typeof log.body === 'object') {
|
||||
const bodyValue = getBodyFieldValue(log.body, fieldName);
|
||||
if (bodyValue !== undefined) {
|
||||
return bodyValue;
|
||||
}
|
||||
}
|
||||
return FlatLogData(log)[fieldName];
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('normalizeFilterExpression', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('lowercases HAS / HASANY / HASALL / HASTOKEN function names', () => {
|
||||
it('lowercases HAS / HASANY / HASALL / HASTOKEN / SEARCH function names', () => {
|
||||
expect(normalizeFilterExpression('HAS(tags, "x")')).toBe(
|
||||
normalizeFilterExpression('has(tags, "x")'),
|
||||
);
|
||||
@@ -47,6 +47,9 @@ describe('normalizeFilterExpression', () => {
|
||||
expect(normalizeFilterExpression('HASTOKEN(msg, "err")')).toBe(
|
||||
normalizeFilterExpression('hasToken(msg, "err")'),
|
||||
);
|
||||
expect(normalizeFilterExpression('SEARCH("err")')).toBe(
|
||||
normalizeFilterExpression('search("err")'),
|
||||
);
|
||||
});
|
||||
|
||||
it('lowercases TRUE / FALSE boolean literals', () => {
|
||||
|
||||
@@ -19,6 +19,30 @@ type CompositeWithBuilder = {
|
||||
builder?: { queryData?: IBuilderQuery[] };
|
||||
};
|
||||
|
||||
export function saveRecentQueryByExpression(
|
||||
dataSource: IBuilderQuery['dataSource'],
|
||||
expression: string | null | undefined,
|
||||
source = '',
|
||||
): void {
|
||||
const trimmed = expression?.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
const validation = validateQuery(trimmed);
|
||||
if (!validation.isValid) {
|
||||
return;
|
||||
}
|
||||
const signal = toSignal(dataSource);
|
||||
if (!signal) {
|
||||
return;
|
||||
}
|
||||
store.save({
|
||||
signal,
|
||||
source,
|
||||
filter: { expression: trimmed },
|
||||
});
|
||||
}
|
||||
|
||||
// Persists each builder query in the composite as a recent entry. Call this
|
||||
// only from explicit user-driven Run triggers — reacting to stagedQuery or any
|
||||
// other derived state pollutes recents with navigation/refresh/go-to traffic.
|
||||
@@ -31,22 +55,10 @@ export function saveRecentQuery(
|
||||
}
|
||||
|
||||
queryData.forEach((q) => {
|
||||
const expression = q.filter?.expression?.trim();
|
||||
if (!expression) {
|
||||
return;
|
||||
}
|
||||
const validation = validateQuery(expression);
|
||||
if (!validation.isValid) {
|
||||
return;
|
||||
}
|
||||
const signal = toSignal(q.dataSource);
|
||||
if (!signal) {
|
||||
return;
|
||||
}
|
||||
store.save({
|
||||
signal,
|
||||
source: q.source ?? '',
|
||||
filter: q.filter ?? { expression: '' },
|
||||
});
|
||||
saveRecentQueryByExpression(
|
||||
q.dataSource,
|
||||
q.filter?.expression,
|
||||
q.source ?? '',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
export const membersResponse = [
|
||||
{
|
||||
id: '3223a874-5678458745786',
|
||||
name: 'John Doe',
|
||||
email: 'firstUser@test.io',
|
||||
createdAt: 1666357530,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '5e9681b1-5678458745786',
|
||||
name: 'Jane Doe',
|
||||
email: 'johndoe2@test.io',
|
||||
createdAt: 1666365394,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '11e8c55d-5678458745786',
|
||||
name: 'Alex',
|
||||
email: 'blah@test.io',
|
||||
createdAt: 1666366317,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: 'd878012367813286731aab62',
|
||||
role: 'VIEWER',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '2ad2e404-5678458745786',
|
||||
name: 'Tom',
|
||||
email: 'johndoe4@test.io',
|
||||
createdAt: 1673441483,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '6f532456-5678458745786',
|
||||
name: 'Harry',
|
||||
email: 'harry@test.io',
|
||||
createdAt: 1691551672,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: 'ae22fa73-5678458745786',
|
||||
name: 'Ron',
|
||||
email: 'ron@test.io',
|
||||
createdAt: 1691668239,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '3223a874-5678458745786',
|
||||
name: 'John Doe',
|
||||
email: 'johndoe@test.io',
|
||||
createdAt: 1666357530,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '5e9681b1-5678458745786',
|
||||
name: 'Jane Doe',
|
||||
email: 'johndoe2@test.io',
|
||||
createdAt: 1666365394,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '11e8c55d-5678458745786',
|
||||
name: 'Alex',
|
||||
email: 'blah@test.io',
|
||||
createdAt: 1666366317,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: 'd878012367813286731aab62',
|
||||
role: 'VIEWER',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '2ad2e404-5678458745786',
|
||||
name: 'Tom',
|
||||
email: 'johndoe4@test.io',
|
||||
createdAt: 1673441483,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '6f532456-5678458745786',
|
||||
name: 'Harry',
|
||||
email: 'harry@test.io',
|
||||
createdAt: 1691551672,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: 'ae22fa73-5678458745786',
|
||||
name: 'Ron',
|
||||
email: 'ron@test.io',
|
||||
createdAt: 1691668239,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '3223a874-5678458745786',
|
||||
name: 'John Doe',
|
||||
email: 'johndoe@test.io',
|
||||
createdAt: 1666357530,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '5e9681b1-5678458745786',
|
||||
name: 'Jane Doe',
|
||||
email: 'johndoe2@test.io',
|
||||
createdAt: 1666365394,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '11e8c55d-5678458745786',
|
||||
name: 'Alex',
|
||||
email: 'blah@test.io',
|
||||
createdAt: 1666366317,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: 'd878012367813286731aab62',
|
||||
role: 'VIEWER',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '2ad2e404-5678458745786',
|
||||
name: 'Tom',
|
||||
email: 'johndoe4@test.io',
|
||||
createdAt: 1673441483,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '6f532456-5678458745786',
|
||||
name: 'Harry',
|
||||
email: 'harry@test.io',
|
||||
createdAt: 1691551672,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: 'ae22fa73-5678458745786',
|
||||
name: 'Ron',
|
||||
email: 'lastUser@test.io',
|
||||
createdAt: 1691668239,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
];
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from './__mockdata__/dashboards';
|
||||
import { explorerView } from './__mockdata__/explorer_views';
|
||||
import { licensesSuccessResponse } from './__mockdata__/licenses';
|
||||
import { membersResponse } from './__mockdata__/members';
|
||||
import { queryRangeSuccessResponse } from './__mockdata__/query_range';
|
||||
import { serviceSuccessResponse } from './__mockdata__/services';
|
||||
import { topLevelOperationSuccessResponse } from './__mockdata__/top_level_operations';
|
||||
@@ -40,9 +39,6 @@ export const handlers = [
|
||||
res(ctx.status(200), ctx.json(topLevelOperationSuccessResponse)),
|
||||
),
|
||||
|
||||
rest.get('http://localhost/api/v1/user', (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: '200', data: membersResponse })),
|
||||
),
|
||||
rest.get(
|
||||
'http://localhost/api/v3/autocomplete/attribute_keys',
|
||||
(req, res, ctx) => {
|
||||
@@ -164,15 +160,6 @@ export const handlers = [
|
||||
),
|
||||
),
|
||||
|
||||
rest.post('http://localhost/api/v1/invite', (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: 'invite sent successfully',
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'http://localhost/api/v3/autocomplete/aggregate_attributes',
|
||||
(req, res, ctx) =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//@ts-nocheck
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { connect } from 'react-redux';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
@@ -11,6 +11,7 @@ import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
|
||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||
import { whilelistedKeys } from 'hooks/useResourceAttribute/config';
|
||||
import { IResourceAttribute } from 'hooks/useResourceAttribute/types';
|
||||
import { filterServiceMapSupportedQueries } from 'hooks/useResourceAttribute/utils';
|
||||
import { getDetailedServiceMapItems, ServiceMapStore } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import styled from 'styled-components';
|
||||
@@ -70,32 +71,37 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
|
||||
|
||||
const { queries } = useResourceAttribute();
|
||||
|
||||
const supportedQueries = useMemo(
|
||||
() => filterServiceMapSupportedQueries(queries),
|
||||
[queries],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
Call the apis only when the route is loaded.
|
||||
Check this issue: https://github.com/SigNoz/signoz/issues/110
|
||||
*/
|
||||
getDetailedServiceMapItems(globalTime, queries);
|
||||
}, [globalTime, getDetailedServiceMapItems, queries]);
|
||||
getDetailedServiceMapItems(globalTime, supportedQueries);
|
||||
}, [globalTime, getDetailedServiceMapItems, supportedQueries]);
|
||||
|
||||
useEffect(() => {
|
||||
fgRef.current && fgRef.current.d3Force('charge').strength(-400);
|
||||
});
|
||||
|
||||
if (serviceMap.loading) {
|
||||
return <Spinner size="large" tip="Loading..." />;
|
||||
}
|
||||
const renderBody = (): JSX.Element => {
|
||||
if (serviceMap.loading) {
|
||||
return <Spinner size="large" tip="Loading..." />;
|
||||
}
|
||||
|
||||
if (serviceMap.items.length === 0) {
|
||||
return <Card>No Service Found</Card>;
|
||||
}
|
||||
|
||||
return <Map fgRef={fgRef} serviceMap={serviceMap} />;
|
||||
};
|
||||
|
||||
if (!serviceMap.loading && serviceMap.items.length === 0) {
|
||||
return (
|
||||
<Container>
|
||||
<ResourceAttributesFilter />
|
||||
<Card>No Service Found</Card>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="service-map-container">
|
||||
<Container className="service-map-container">
|
||||
<ResourceAttributesFilter
|
||||
suffixIcon={
|
||||
<TextToolTip
|
||||
@@ -108,8 +114,8 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
|
||||
}
|
||||
/>
|
||||
|
||||
<Map fgRef={fgRef} serviceMap={serviceMap} />
|
||||
</div>
|
||||
{renderBody()}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user