mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-19 01:40:41 +01:00
Compare commits
4 Commits
nv/area-ch
...
removal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3de6f2e72c | ||
|
|
be9170ced6 | ||
|
|
2068482f66 | ||
|
|
35973efd65 |
@@ -171,6 +171,14 @@ components:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelDefect:
|
||||
enum:
|
||||
- none
|
||||
- missing_type
|
||||
- multiple_notifiers
|
||||
- unsupported_notifier
|
||||
- unrepresentable
|
||||
type: string
|
||||
AlertmanagertypesChannelEmailConfig:
|
||||
properties:
|
||||
headers:
|
||||
@@ -384,6 +392,40 @@ components:
|
||||
required:
|
||||
- routingKey
|
||||
type: object
|
||||
AlertmanagertypesChannelRepair:
|
||||
properties:
|
||||
action:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelRepairAction'
|
||||
applied:
|
||||
type: boolean
|
||||
blockers:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
channels:
|
||||
items:
|
||||
$ref: '#/components/schemas/AlertmanagertypesListedNotificationChannel'
|
||||
nullable: true
|
||||
type: array
|
||||
defect:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelDefect'
|
||||
detail:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- defect
|
||||
- action
|
||||
- applied
|
||||
type: object
|
||||
AlertmanagertypesChannelRepairAction:
|
||||
enum:
|
||||
- none
|
||||
- retype
|
||||
- split
|
||||
- delete
|
||||
type: string
|
||||
AlertmanagertypesChannelSlackAction:
|
||||
properties:
|
||||
confirm:
|
||||
@@ -1033,6 +1075,11 @@ components:
|
||||
- duration
|
||||
- repeatType
|
||||
type: object
|
||||
AlertmanagertypesRepairChannelParams:
|
||||
properties:
|
||||
apply:
|
||||
type: boolean
|
||||
type: object
|
||||
AlertmanagertypesRepeatOn:
|
||||
enum:
|
||||
- sunday
|
||||
@@ -20306,6 +20353,85 @@ paths:
|
||||
summary: Update notification channel
|
||||
tags:
|
||||
- channels
|
||||
/api/v2/notification_channels/{id}/repair:
|
||||
post:
|
||||
deprecated: false
|
||||
description: 'This endpoint diagnoses a stored channel that the v2 API cannot
|
||||
read and applies the fitting action: a channel carrying several notifier configurations
|
||||
is split into one channel per configuration, keeping this ID for the first;
|
||||
a channel whose notifier kind v2 does not model is deleted; a channel with
|
||||
an empty stored type has it rewritten from its data. A delete is refused while
|
||||
a routing policy still names the channel. Nothing is written unless apply=true;
|
||||
by default the response only shows what would happen.'
|
||||
operationId: RepairNotificationChannel
|
||||
parameters:
|
||||
- in: query
|
||||
name: apply
|
||||
schema:
|
||||
type: boolean
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlertmanagertypesRepairChannelParams'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelRepair'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- notification-channel:update
|
||||
- tokenizer:
|
||||
- notification-channel:update
|
||||
summary: Repair notification channel
|
||||
tags:
|
||||
- channels
|
||||
/api/v2/notification_channels/test:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||
"LOGS": "SigNoz | Logs",
|
||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||
"OLD_LOGS_EXPLORER": "SigNoz | Old Logs Explorer",
|
||||
"LIVE_LOGS": "SigNoz | Live Logs",
|
||||
"LOGS_PIPELINES": "SigNoz | Logs Pipelines",
|
||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||
|
||||
@@ -1602,10 +1602,6 @@ describe('PrivateRoute', () => {
|
||||
LOGS: { path: ROUTES.LOGS, deniedRoles: DENIED_ROLES },
|
||||
LOGS_EXPLORER: { path: ROUTES.LOGS_EXPLORER, deniedRoles: DENIED_ROLES },
|
||||
LIVE_LOGS: { path: ROUTES.LIVE_LOGS, deniedRoles: DENIED_ROLES },
|
||||
OLD_LOGS_EXPLORER: {
|
||||
path: ROUTES.OLD_LOGS_EXPLORER,
|
||||
deniedRoles: DENIED_ROLES,
|
||||
},
|
||||
METRICS_EXPLORER: {
|
||||
path: ROUTES.METRICS_EXPLORER,
|
||||
deniedRoles: DENIED_ROLES,
|
||||
|
||||
@@ -169,10 +169,6 @@ export const LogsExplorer = Loadable(
|
||||
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsModulePage'),
|
||||
);
|
||||
|
||||
export const OldLogsExplorer = Loadable(
|
||||
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/Logs'),
|
||||
);
|
||||
|
||||
export const LiveLogs = Loadable(
|
||||
() => import(/* webpackChunkName: "Live Logs" */ 'pages/LiveLogs'),
|
||||
);
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
MessagingQueuesMainPage,
|
||||
MeterExplorerPage,
|
||||
MetricsExplorer,
|
||||
OldLogsExplorer,
|
||||
OnboardingV2,
|
||||
OrgOnboarding,
|
||||
PasswordReset,
|
||||
@@ -308,13 +307,6 @@ const routes: AppRoutes[] = [
|
||||
key: 'LOGS_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.OLD_LOGS_EXPLORER,
|
||||
exact: true,
|
||||
component: OldLogsExplorer,
|
||||
key: 'OLD_LOGS_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LIVE_LOGS,
|
||||
exact: true,
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
AlertmanagertypesPostableChannelDTO,
|
||||
AlertmanagertypesPostableNotificationChannelDTO,
|
||||
AlertmanagertypesReceiverDTO,
|
||||
AlertmanagertypesRepairChannelParamsDTO,
|
||||
AlertmanagertypesTestableNotificationChannelDTO,
|
||||
AlertmanagertypesUpdatableNotificationChannelDTO,
|
||||
CreateChannel201,
|
||||
@@ -35,6 +36,9 @@ import type {
|
||||
ListNotificationChannels200,
|
||||
ListNotificationChannelsParams,
|
||||
RenderErrorResponseDTO,
|
||||
RepairNotificationChannel200,
|
||||
RepairNotificationChannelParams,
|
||||
RepairNotificationChannelPathParameters,
|
||||
UpdateChannelByIDPathParameters,
|
||||
UpdateNotificationChannel200,
|
||||
UpdateNotificationChannelPathParameters,
|
||||
@@ -1144,6 +1148,113 @@ export const useUpdateNotificationChannel = <
|
||||
> => {
|
||||
return useMutation(getUpdateNotificationChannelMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint diagnoses a stored channel that the v2 API cannot read and applies the fitting action: a channel carrying several notifier configurations is split into one channel per configuration, keeping this ID for the first; a channel whose notifier kind v2 does not model is deleted; a channel with an empty stored type has it rewritten from its data. A delete is refused while a routing policy still names the channel. Nothing is written unless apply=true; by default the response only shows what would happen.
|
||||
* @summary Repair notification channel
|
||||
*/
|
||||
export const repairNotificationChannel = (
|
||||
{ id }: RepairNotificationChannelPathParameters,
|
||||
alertmanagertypesRepairChannelParamsDTO?: BodyType<AlertmanagertypesRepairChannelParamsDTO>,
|
||||
params?: RepairNotificationChannelParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<RepairNotificationChannel200>({
|
||||
url: `/api/v2/notification_channels/${id}/repair`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: alertmanagertypesRepairChannelParamsDTO,
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getRepairNotificationChannelMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof repairNotificationChannel>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: RepairNotificationChannelPathParameters;
|
||||
data?: BodyType<AlertmanagertypesRepairChannelParamsDTO>;
|
||||
params?: RepairNotificationChannelParams;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof repairNotificationChannel>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: RepairNotificationChannelPathParameters;
|
||||
data?: BodyType<AlertmanagertypesRepairChannelParamsDTO>;
|
||||
params?: RepairNotificationChannelParams;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['repairNotificationChannel'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof repairNotificationChannel>>,
|
||||
{
|
||||
pathParams: RepairNotificationChannelPathParameters;
|
||||
data?: BodyType<AlertmanagertypesRepairChannelParamsDTO>;
|
||||
params?: RepairNotificationChannelParams;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data, params } = props ?? {};
|
||||
|
||||
return repairNotificationChannel(pathParams, data, params);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type RepairNotificationChannelMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof repairNotificationChannel>>
|
||||
>;
|
||||
export type RepairNotificationChannelMutationBody =
|
||||
| BodyType<AlertmanagertypesRepairChannelParamsDTO>
|
||||
| undefined;
|
||||
export type RepairNotificationChannelMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Repair notification channel
|
||||
*/
|
||||
export const useRepairNotificationChannel = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof repairNotificationChannel>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: RepairNotificationChannelPathParameters;
|
||||
data?: BodyType<AlertmanagertypesRepairChannelParamsDTO>;
|
||||
params?: RepairNotificationChannelParams;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof repairNotificationChannel>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: RepairNotificationChannelPathParameters;
|
||||
data?: BodyType<AlertmanagertypesRepairChannelParamsDTO>;
|
||||
params?: RepairNotificationChannelParams;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getRepairNotificationChannelMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint sends a test notification for the configuration in the request body. The channel need not exist and nothing is persisted, so the body carries a configuration only.
|
||||
* @summary Test notification channel
|
||||
|
||||
@@ -596,6 +596,13 @@ export type AlertmanagertypesChannelConfigDTO =
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJSMOpsConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelIncidentIOConfigDTO;
|
||||
|
||||
export enum AlertmanagertypesChannelDefectDTO {
|
||||
none = 'none',
|
||||
missing_type = 'missing_type',
|
||||
multiple_notifiers = 'multiple_notifiers',
|
||||
unsupported_notifier = 'unsupported_notifier',
|
||||
unrepresentable = 'unrepresentable',
|
||||
}
|
||||
export enum AlertmanagertypesChannelKindDTO {
|
||||
slack = 'slack',
|
||||
email = 'email',
|
||||
@@ -617,6 +624,63 @@ export enum AlertmanagertypesChannelListSortDTO {
|
||||
created_at = 'created_at',
|
||||
name = 'name',
|
||||
}
|
||||
export enum AlertmanagertypesChannelRepairActionDTO {
|
||||
none = 'none',
|
||||
retype = 'retype',
|
||||
split = 'split',
|
||||
delete = 'delete',
|
||||
}
|
||||
export interface AlertmanagertypesListedNotificationChannelDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
displayName: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
kind: AlertmanagertypesChannelKindDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesChannelRepairDTO {
|
||||
action: AlertmanagertypesChannelRepairActionDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
applied: boolean;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
blockers?: string[];
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
channels?: AlertmanagertypesListedNotificationChannelDTO[] | null;
|
||||
defect: AlertmanagertypesChannelDefectDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
detail?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ModelLabelSetDTO {
|
||||
[key: string]: string;
|
||||
}
|
||||
@@ -1110,32 +1174,6 @@ export interface AlertmanagertypesJiraReceiverConfigDTO {
|
||||
wont_fix_resolution?: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesListedNotificationChannelDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
displayName: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
kind: AlertmanagertypesChannelKindDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesListableNotificationChannelDTO {
|
||||
/**
|
||||
* @type array
|
||||
@@ -2539,6 +2577,13 @@ export interface AlertmanagertypesReceiverDTO {
|
||||
wechat_configs?: ConfigWechatConfigDTO[];
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesRepairChannelParamsDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
apply?: boolean;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesTestableNotificationChannelDTO {
|
||||
config: AlertmanagertypesChannelConfigDTO;
|
||||
}
|
||||
@@ -13484,6 +13529,25 @@ export type UpdateNotificationChannel200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type RepairNotificationChannelPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type RepairNotificationChannelParams = {
|
||||
/**
|
||||
* @type boolean
|
||||
* @description undefined
|
||||
*/
|
||||
apply?: boolean;
|
||||
};
|
||||
|
||||
export type RepairNotificationChannel200 = {
|
||||
data: AlertmanagertypesChannelRepairDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetMyOrganization200 = {
|
||||
data: TypesOrganizationDTO;
|
||||
/**
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/logs/addToSelectedFields';
|
||||
|
||||
const addToSelectedFields = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const data = await axios.post(`/logs/fields`, props);
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return Promise.reject(ErrorResponseHandler(error as AxiosError));
|
||||
}
|
||||
};
|
||||
|
||||
export default addToSelectedFields;
|
||||
@@ -1,26 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/logs/getLogs';
|
||||
|
||||
const GetLogs = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const data = await axios.get(`/logs`, {
|
||||
params: props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data.results,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default GetLogs;
|
||||
@@ -1,26 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/logs/getLogsAggregate';
|
||||
|
||||
const GetLogsAggregate = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const data = await axios.get(`/logs/aggregate`, {
|
||||
params: props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data.items,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default GetLogsAggregate;
|
||||
@@ -1,24 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/logs/getSearchFields';
|
||||
|
||||
const GetSearchFields = async (): Promise<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const data = await axios.get(`/logs/fields`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default GetSearchFields;
|
||||
@@ -1,23 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/logs/addToSelectedFields';
|
||||
|
||||
const removeSelectedField = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const data = await axios.post(`/logs/fields`, props);
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return Promise.reject(ErrorResponseHandler(error as AxiosError));
|
||||
}
|
||||
};
|
||||
|
||||
export default removeSelectedField;
|
||||
@@ -1,22 +0,0 @@
|
||||
import apiV1 from 'api/apiV1';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import { withBasePath } from 'utils/basePath';
|
||||
|
||||
// 10 min in ms
|
||||
const TIMEOUT_IN_MS = 10 * 60 * 1000;
|
||||
|
||||
export const LiveTail = (queryParams: string): EventSourcePolyfill =>
|
||||
new EventSourcePolyfill(
|
||||
ENVIRONMENT.baseURL
|
||||
? `${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`
|
||||
: withBasePath(`${apiV1}logs/tail?${queryParams}`),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${getLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN)}`,
|
||||
},
|
||||
heartbeatTimeout: TIMEOUT_IN_MS,
|
||||
},
|
||||
);
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, InputNumber, Popover, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { LogViewMode } from 'container/OptionsMenu/types';
|
||||
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||
import {
|
||||
Check,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import CheckboxFilterHeader from './CheckboxFilterHeader';
|
||||
import CheckboxValueRow from './CheckboxValueRow';
|
||||
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
|
||||
import useActiveQueryIndex from './useActiveQueryIndex';
|
||||
import useActiveQueryIndex from 'components/QuickFilters/hooks/useActiveQueryIndex';
|
||||
import useCheckboxDisclosure from './useCheckboxDisclosure';
|
||||
import useCheckboxFilterActions from './useCheckboxFilterActions';
|
||||
import useCheckboxFilterState from './useCheckboxFilterState';
|
||||
|
||||
@@ -56,6 +56,57 @@ export function mockFieldsValuesAPI(response: {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records every request the AI observability values endpoint receives, so a test
|
||||
* can assert both the routing and the query params it was called with.
|
||||
*/
|
||||
export function mockAIObservabilityFieldsValuesAPI(response: {
|
||||
relatedValues?: (string | null)[];
|
||||
stringValues?: (string | null)[];
|
||||
numberValues?: (number | null)[];
|
||||
}): { requests: URLSearchParams[] } {
|
||||
const requests: URLSearchParams[] = [];
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
'http://localhost/api/v1/ai_observability/fields/values',
|
||||
(req, res, ctx) => {
|
||||
requests.push(req.url.searchParams);
|
||||
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
values: {
|
||||
relatedValues: response.relatedValues ?? [],
|
||||
stringValues: response.stringValues ?? [],
|
||||
numberValues: response.numberValues ?? [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return { requests };
|
||||
}
|
||||
|
||||
/** Fails the test if the signal-wide values endpoint is hit at all. */
|
||||
export function forbidFieldsValuesAPI(): { called: boolean } {
|
||||
const state = { called: false };
|
||||
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/fields/values', (_, res, ctx) => {
|
||||
state.called = true;
|
||||
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
|
||||
}),
|
||||
);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export function mockFieldsValuesAPILoading(): void {
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/fields/values', (_, res, ctx) =>
|
||||
|
||||
@@ -16,7 +16,7 @@ import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { NON_SELECTED_OPERATORS } from '../checkboxFilterQuery';
|
||||
import useActiveQueryIndex from '../useActiveQueryIndex';
|
||||
import useActiveQueryIndex from 'components/QuickFilters/hooks/useActiveQueryIndex';
|
||||
import useCheckboxDisclosure from '../useCheckboxDisclosure';
|
||||
import useCheckboxFilterActions from '../useCheckboxFilterActions';
|
||||
import useCheckboxFilterState from '../useCheckboxFilterState';
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { render } from 'tests/test-utils';
|
||||
|
||||
import { QuickFiltersSource } from '../../../../types';
|
||||
|
||||
import CheckboxFilterV2 from '../CheckboxFilterV2';
|
||||
import {
|
||||
DEFAULT_FILTER,
|
||||
DEFAULT_USE_FIELD_APIS,
|
||||
forbidFieldsValuesAPI,
|
||||
mockAIObservabilityFieldsValuesAPI,
|
||||
mockFieldsValuesAPI,
|
||||
setupServer,
|
||||
} from '../CheckboxFilterV2.testUtils';
|
||||
|
||||
setupServer();
|
||||
|
||||
describe('CheckboxFilterV2 - AI observability routing', () => {
|
||||
it('reads values from the AI observability endpoint and never the signal-wide one', async () => {
|
||||
const aiEndpoint = mockAIObservabilityFieldsValuesAPI({
|
||||
stringValues: ['openai', 'anthropic'],
|
||||
});
|
||||
const fieldsEndpoint = forbidFieldsValuesAPI();
|
||||
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(screen.findByText('openai')).resolves.toBeInTheDocument();
|
||||
expect(screen.getByText('anthropic')).toBeInTheDocument();
|
||||
expect(fieldsEndpoint.called).toBe(false);
|
||||
expect(aiEndpoint.requests).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('forwards the filter key and the time range to the AI observability endpoint', async () => {
|
||||
const aiEndpoint = mockAIObservabilityFieldsValuesAPI({
|
||||
stringValues: ['openai'],
|
||||
});
|
||||
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText('openai');
|
||||
|
||||
const params = aiEndpoint.requests[0];
|
||||
expect(params.get('name')).toBe(DEFAULT_FILTER.attributeKey.key);
|
||||
expect(params.get('startUnixMilli')).toBe(
|
||||
String(DEFAULT_USE_FIELD_APIS.startUnixMilli),
|
||||
);
|
||||
expect(params.get('endUnixMilli')).toBe(
|
||||
String(DEFAULT_USE_FIELD_APIS.endUnixMilli),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps non-AI sources on the signal-wide endpoint', async () => {
|
||||
mockFieldsValuesAPI({ stringValues: ['production'] });
|
||||
const aiEndpoint = mockAIObservabilityFieldsValuesAPI({
|
||||
stringValues: ['should-not-be-used'],
|
||||
});
|
||||
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(screen.findByText('production')).resolves.toBeInTheDocument();
|
||||
await waitFor(() => expect(aiEndpoint.requests).toHaveLength(0));
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useGetFieldsValues } from 'api/generated/services/fields';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { FieldValuesConfig } from 'api/querySuggestions/types';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { FIELD_API_CACHE_TIME } from 'constants/queryCacheTime';
|
||||
import { useFieldValuesSuggestion } from 'hooks/querySuggestions/useFieldValuesSuggestion';
|
||||
import { BuilderQueryType } from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL } from 'types/common/queryBuilder';
|
||||
|
||||
interface UseFieldValuesProps {
|
||||
@@ -42,32 +43,43 @@ export function useFieldValues({
|
||||
endUnixMilli,
|
||||
enabled,
|
||||
}: UseFieldValuesProps): UseFieldValuesReturn {
|
||||
const { data, isLoading, isFetching } = useGetFieldsValues(
|
||||
{
|
||||
signal: filter.dataSource
|
||||
? DATA_SOURCE_TO_SIGNAL[filter.dataSource]
|
||||
: undefined,
|
||||
name: filter.attributeKey.key,
|
||||
searchText,
|
||||
existingQuery,
|
||||
metricNamespace,
|
||||
source: source ? QUICK_FILTERS_SOURCE_TO_SOURCE[source] : undefined,
|
||||
startUnixMilli,
|
||||
// This field does not affect the backend but I wanted to keep it here
|
||||
// in case we add the support in the future
|
||||
endUnixMilli,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
enabled,
|
||||
cacheTime: FIELD_API_CACHE_TIME,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
const isAIObservability = source === QuickFiltersSource.AI_OBSERVABILITY;
|
||||
|
||||
const builderQueryType: BuilderQueryType | undefined = isAIObservability
|
||||
? 'builder_ai_query'
|
||||
: undefined;
|
||||
|
||||
// The AI values endpoint is already gen_ai-scoped: no signal, no source.
|
||||
const fieldValuesConfig: FieldValuesConfig = isAIObservability
|
||||
? {
|
||||
name: filter.attributeKey.key,
|
||||
searchText,
|
||||
existingQuery,
|
||||
startUnixMilli,
|
||||
endUnixMilli,
|
||||
}
|
||||
: {
|
||||
signal: filter.dataSource
|
||||
? DATA_SOURCE_TO_SIGNAL[filter.dataSource]
|
||||
: undefined,
|
||||
name: filter.attributeKey.key,
|
||||
searchText,
|
||||
existingQuery,
|
||||
metricNamespace,
|
||||
source: source ? QUICK_FILTERS_SOURCE_TO_SOURCE[source] : undefined,
|
||||
startUnixMilli,
|
||||
// This field does not affect the backend but I wanted to keep it here
|
||||
// in case we add the support in the future
|
||||
endUnixMilli,
|
||||
};
|
||||
|
||||
const {
|
||||
data: values,
|
||||
isLoading,
|
||||
isFetching,
|
||||
} = useFieldValuesSuggestion(fieldValuesConfig, builderQueryType, { enabled });
|
||||
|
||||
const relatedValues: string[] = useMemo(() => {
|
||||
const values = data?.data?.values;
|
||||
if (!values) {
|
||||
return [];
|
||||
}
|
||||
@@ -78,10 +90,9 @@ export function useFieldValues({
|
||||
value !== null && value !== undefined && value !== '',
|
||||
) || []
|
||||
);
|
||||
}, [data]);
|
||||
}, [values]);
|
||||
|
||||
const allValues: string[] = useMemo(() => {
|
||||
const values = data?.data?.values;
|
||||
if (!values) {
|
||||
return [];
|
||||
}
|
||||
@@ -101,7 +112,7 @@ export function useFieldValues({
|
||||
.map((value) => value.toString()) || [];
|
||||
|
||||
return [...stringValues, ...numberValues, ...boolValues];
|
||||
}, [data]);
|
||||
}, [values]);
|
||||
|
||||
return { relatedValues, allValues, isLoading, isFetching };
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Collapse } from 'antd';
|
||||
import { Undo2 } from '@signozhq/icons';
|
||||
import useActiveQueryIndex from 'components/QuickFilters/hooks/useActiveQueryIndex';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
@@ -39,7 +39,7 @@ function Duration({
|
||||
}: {
|
||||
filter: IQuickFiltersConfig;
|
||||
onFilterChange?: (query: Query) => void;
|
||||
source?: QuickFiltersSource;
|
||||
source: QuickFiltersSource;
|
||||
}): JSX.Element {
|
||||
const [selectedFilters, setSelectedFilters] =
|
||||
useState<
|
||||
@@ -52,26 +52,11 @@ function Duration({
|
||||
filter.defaultOpen ? 'durationNano' : '',
|
||||
]);
|
||||
|
||||
const {
|
||||
currentQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
lastUsedQuery,
|
||||
panelType,
|
||||
} = useQueryBuilder();
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
|
||||
const isListView = panelType === PANEL_TYPES.LIST;
|
||||
// In ListView mode, use index 0 for most sources; for TRACES_EXPLORER, use lastUsedQuery
|
||||
// Otherwise use lastUsedQuery for non-ListView modes
|
||||
const activeQueryIndex = useMemo(() => {
|
||||
if (isListView) {
|
||||
return source === QuickFiltersSource.TRACES_EXPLORER
|
||||
? lastUsedQuery || 0
|
||||
: 0;
|
||||
}
|
||||
return lastUsedQuery || 0;
|
||||
}, [isListView, source, lastUsedQuery]);
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const syncSelectedFilters = useMemo((): FilterType => {
|
||||
|
||||
@@ -35,6 +35,7 @@ import { isFunction } from 'lodash-es';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
|
||||
import useActiveQueryIndex from './hooks/useActiveQueryIndex';
|
||||
import CheckboxV2 from './FilterRenderers/Checkbox/v2/CheckboxFilterV2';
|
||||
import Duration from './FilterRenderers/Duration/Duration';
|
||||
import Slider from './FilterRenderers/Slider/Slider';
|
||||
@@ -113,14 +114,13 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
const shouldShowDropdownInListView =
|
||||
isListView && source === QuickFiltersSource.TRACES_EXPLORER;
|
||||
|
||||
const activeQueryIndex = useMemo(() => {
|
||||
if (isListView) {
|
||||
return source === QuickFiltersSource.TRACES_EXPLORER
|
||||
? lastUsedQuery || 0
|
||||
: 0;
|
||||
}
|
||||
return lastUsedQuery || 0;
|
||||
}, [isListView, source, lastUsedQuery]);
|
||||
// AI observability builds a single query in the row-level views, so there is
|
||||
// no query for the selector to switch between.
|
||||
const isAIObservabilityRowView =
|
||||
source === QuickFiltersSource.AI_OBSERVABILITY &&
|
||||
(isListView || panelType === PANEL_TYPES.TRACE);
|
||||
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
|
||||
// clear all the filters for the query which is in sync with filters
|
||||
const handleReset = (): void => {
|
||||
@@ -167,9 +167,10 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
|
||||
|
||||
// In ListView, always show the 0th query's name; otherwise use the active query's name
|
||||
const displayedQueryName = isListView
|
||||
? showQueryName && currentQuery.builder.queryData?.[0]?.queryName
|
||||
: lastQueryName;
|
||||
const displayedQueryName =
|
||||
isListView || isAIObservabilityRowView
|
||||
? showQueryName && currentQuery.builder.queryData?.[0]?.queryName
|
||||
: lastQueryName;
|
||||
|
||||
const handleQueryChange = (value: number): void => {
|
||||
setLastUsedQuery(value);
|
||||
@@ -182,7 +183,9 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
<Typography.Text className="text">
|
||||
{displayedQueryName ? 'Filters for' : 'Filters'}
|
||||
</Typography.Text>
|
||||
{queryOptions.length > 1 && (!isListView || shouldShowDropdownInListView) ? (
|
||||
{queryOptions.length > 1 &&
|
||||
!isAIObservabilityRowView &&
|
||||
(!isListView || shouldShowDropdownInListView) ? (
|
||||
<Combobox open={open} onOpenChange={setOpen}>
|
||||
<ComboboxTrigger
|
||||
placeholder="Select a query"
|
||||
@@ -318,6 +321,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
return (
|
||||
<Duration
|
||||
key={filter.attributeKey.key}
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import { useGetFieldsKeys } from 'api/generated/services/fields';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { FieldKeysConfig } from 'api/querySuggestions/types';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
|
||||
import {
|
||||
BuilderQueryType,
|
||||
FieldContext,
|
||||
FieldDataType,
|
||||
TelemetryFieldKey,
|
||||
@@ -41,23 +43,31 @@ function OtherFilters({
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
|
||||
}): JSX.Element {
|
||||
const isMeterDataSource = signal === SignalType.METER_EXPLORER;
|
||||
const isAIObservability = signal === SignalType.AI_OBSERVABILITY;
|
||||
|
||||
const { data, isFetching } = useGetFieldsKeys(
|
||||
{
|
||||
searchText: inputValue,
|
||||
signal: signal
|
||||
? DATA_SOURCE_TO_SIGNAL[SIGNAL_DATA_SOURCE_MAP[signal]]
|
||||
: undefined,
|
||||
source: isMeterDataSource ? TelemetrytypesSourceDTO.meter : undefined,
|
||||
},
|
||||
{ query: { enabled: !!signal } },
|
||||
const builderQueryType: BuilderQueryType | undefined = isAIObservability
|
||||
? 'builder_ai_query'
|
||||
: undefined;
|
||||
|
||||
const fieldKeysConfig: FieldKeysConfig = isAIObservability
|
||||
? { searchText: inputValue }
|
||||
: {
|
||||
searchText: inputValue,
|
||||
signal: signal
|
||||
? DATA_SOURCE_TO_SIGNAL[SIGNAL_DATA_SOURCE_MAP[signal]]
|
||||
: undefined,
|
||||
source: isMeterDataSource ? TelemetrytypesSourceDTO.meter : undefined,
|
||||
};
|
||||
|
||||
const { data: fetchedKeys, isFetching } = useFieldKeysSuggestion(
|
||||
fieldKeysConfig,
|
||||
builderQueryType,
|
||||
);
|
||||
|
||||
const otherFilters = useMemo<TelemetryFieldKey[]>(() => {
|
||||
const rawSuggestions = Object.values(data?.data?.keys ?? {}).flat();
|
||||
// Normalize: synthesize the composite `key` once so downstream reads (dedupe,
|
||||
// add, render) can trust it.
|
||||
const suggestions: TelemetryFieldKey[] = rawSuggestions.map((attr) => ({
|
||||
const suggestions: TelemetryFieldKey[] = (fetchedKeys ?? []).map((attr) => ({
|
||||
name: attr.name,
|
||||
signal: attr.signal as TelemetryFieldKey['signal'],
|
||||
fieldContext: attr.fieldContext as FieldContext,
|
||||
@@ -71,7 +81,7 @@ function OtherFilters({
|
||||
),
|
||||
);
|
||||
return suggestions.filter((attr) => !addedKeys.has(attr.key as string));
|
||||
}, [data, addedFilters]);
|
||||
}, [fetchedKeys, addedFilters]);
|
||||
|
||||
const handleAddFilter = (filter: TelemetryFieldKey): void => {
|
||||
setAddedFilters((prev) => [...prev, filter]);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render } from 'tests/test-utils';
|
||||
|
||||
import { SignalType } from '../../types';
|
||||
import OtherFilters from '../OtherFilters';
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
const FIELDS_KEYS_URL = `${BASE_URL}/api/v1/fields/keys`;
|
||||
const AI_KEYS_URL = `${BASE_URL}/api/v1/ai_observability/fields/keys`;
|
||||
|
||||
function keysResponse(name: string): Record<string, unknown> {
|
||||
return {
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: {
|
||||
[name]: [{ name, fieldContext: 'attribute', fieldDataType: 'string' }],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('OtherFilters - AI observability keys', () => {
|
||||
let fieldsKeysCalled: boolean;
|
||||
let aiKeysParams: URLSearchParams | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
fieldsKeysCalled = false;
|
||||
aiKeysParams = undefined;
|
||||
|
||||
server.use(
|
||||
rest.get(FIELDS_KEYS_URL, (_, res, ctx) => {
|
||||
fieldsKeysCalled = true;
|
||||
return res(ctx.status(200), ctx.json(keysResponse('http.route')));
|
||||
}),
|
||||
rest.get(AI_KEYS_URL, (req, res, ctx) => {
|
||||
aiKeysParams = req.url.searchParams;
|
||||
return res(ctx.status(200), ctx.json(keysResponse('gen_ai.request.model')));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
function renderOtherFilters(signal: SignalType): void {
|
||||
render(
|
||||
<OtherFilters
|
||||
signal={signal}
|
||||
inputValue=""
|
||||
addedFilters={[]}
|
||||
setAddedFilters={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
it('reads AI observability keys from their own endpoint', async () => {
|
||||
renderOtherFilters(SignalType.AI_OBSERVABILITY);
|
||||
|
||||
await expect(
|
||||
screen.findByText('gen_ai.request.model'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(fieldsKeysCalled).toBe(false);
|
||||
});
|
||||
|
||||
it('does not narrow the AI keys by fieldContext', async () => {
|
||||
renderOtherFilters(SignalType.AI_OBSERVABILITY);
|
||||
|
||||
// A `trace` context would return only the computed per-trace aggregates,
|
||||
// which cannot be filtered on.
|
||||
await waitFor(() => expect(aiKeysParams).toBeDefined());
|
||||
expect(aiKeysParams?.get('fieldContext')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps other signals on the signal-wide keys endpoint', async () => {
|
||||
renderOtherFilters(SignalType.TRACES);
|
||||
|
||||
await expect(screen.findByText('http.route')).resolves.toBeInTheDocument();
|
||||
await waitFor(() => expect(aiKeysParams).toBeUndefined());
|
||||
});
|
||||
});
|
||||
@@ -7,4 +7,5 @@ export const SIGNAL_DATA_SOURCE_MAP = {
|
||||
[SignalType.EXCEPTIONS]: DataSource.TRACES,
|
||||
[SignalType.API_MONITORING]: DataSource.TRACES,
|
||||
[SignalType.METER_EXPLORER]: DataSource.METRICS,
|
||||
[SignalType.AI_OBSERVABILITY]: DataSource.TRACES,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
import { QuickFiltersSource } from '../../types';
|
||||
import useActiveQueryIndex from '../useActiveQueryIndex';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
}));
|
||||
|
||||
const LAST_USED_QUERY = 2;
|
||||
|
||||
function mockQueryBuilder(panelType: PANEL_TYPES): void {
|
||||
(useQueryBuilder as jest.Mock).mockReturnValue({
|
||||
lastUsedQuery: LAST_USED_QUERY,
|
||||
panelType,
|
||||
});
|
||||
}
|
||||
|
||||
describe('useActiveQueryIndex', () => {
|
||||
describe('AI observability builds a single query in the row-level views', () => {
|
||||
it.each([PANEL_TYPES.LIST, PANEL_TYPES.TRACE])(
|
||||
'drives the first query in %s',
|
||||
(panelType) => {
|
||||
mockQueryBuilder(panelType);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useActiveQueryIndex(QuickFiltersSource.AI_OBSERVABILITY),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([PANEL_TYPES.TIME_SERIES, PANEL_TYPES.TABLE])(
|
||||
'follows the last used query in %s',
|
||||
(panelType) => {
|
||||
mockQueryBuilder(panelType);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useActiveQueryIndex(QuickFiltersSource.AI_OBSERVABILITY),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(LAST_USED_QUERY);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('other sources are unchanged', () => {
|
||||
it('lets the traces explorer track the last used query in list view', () => {
|
||||
mockQueryBuilder(PANEL_TYPES.LIST);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useActiveQueryIndex(QuickFiltersSource.TRACES_EXPLORER),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(LAST_USED_QUERY);
|
||||
});
|
||||
|
||||
it('pins single-query sources to the first query in list view', () => {
|
||||
mockQueryBuilder(PANEL_TYPES.LIST);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useActiveQueryIndex(QuickFiltersSource.INFRA_MONITORING),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(0);
|
||||
});
|
||||
|
||||
it('tracks the last used query outside list view', () => {
|
||||
mockQueryBuilder(PANEL_TYPES.TIME_SERIES);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useActiveQueryIndex(QuickFiltersSource.LOGS_EXPLORER),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(LAST_USED_QUERY);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,13 +15,21 @@ function useActiveQueryIndex(source: QuickFiltersSource): number {
|
||||
const isListView = panelType === PANEL_TYPES.LIST;
|
||||
|
||||
return useMemo(() => {
|
||||
// AI observability builds a single query in the row-level views, so its
|
||||
// filters always drive the first one there.
|
||||
if (source === QuickFiltersSource.AI_OBSERVABILITY) {
|
||||
return isListView || panelType === PANEL_TYPES.TRACE
|
||||
? 0
|
||||
: lastUsedQuery || 0;
|
||||
}
|
||||
|
||||
if (isListView) {
|
||||
return source === QuickFiltersSource.TRACES_EXPLORER
|
||||
? lastUsedQuery || 0
|
||||
: 0;
|
||||
}
|
||||
return lastUsedQuery || 0;
|
||||
}, [isListView, source, lastUsedQuery]);
|
||||
}, [isListView, panelType, source, lastUsedQuery]);
|
||||
}
|
||||
|
||||
export default useActiveQueryIndex;
|
||||
@@ -24,6 +24,7 @@ export enum SignalType {
|
||||
API_MONITORING = 'api_monitoring',
|
||||
EXCEPTIONS = 'exceptions',
|
||||
METER_EXPLORER = 'meter',
|
||||
AI_OBSERVABILITY = 'ai_observability',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,6 +70,7 @@ export enum QuickFiltersSource {
|
||||
API_MONITORING = 'api-monitoring',
|
||||
EXCEPTIONS = 'exceptions',
|
||||
METER_EXPLORER = 'meter',
|
||||
AI_OBSERVABILITY = 'ai-observability',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -109,6 +109,9 @@ export const REACT_QUERY_KEY = {
|
||||
// Field Keys Suggestion Query Keys
|
||||
FIELD_KEYS_SUGGESTION: 'FIELD_KEYS_SUGGESTION',
|
||||
|
||||
// Field Values Suggestion Query Keys
|
||||
FIELD_VALUES_SUGGESTION: 'FIELD_VALUES_SUGGESTION',
|
||||
|
||||
// AI Assistant Query Keys
|
||||
AI_ASSISTANT_EMPTY_STATE_CHIPS: 'AI_ASSISTANT_EMPTY_STATE_CHIPS',
|
||||
} as const;
|
||||
|
||||
@@ -38,7 +38,6 @@ const ROUTES = {
|
||||
NOT_FOUND: '/not-found',
|
||||
LOGS_BASE: '/logs',
|
||||
LOGS: '/logs/logs-explorer',
|
||||
OLD_LOGS_EXPLORER: '/logs/old-logs-explorer',
|
||||
LOGS_EXPLORER: '/logs/logs-explorer',
|
||||
LIVE_LOGS: '/logs/logs-explorer/live',
|
||||
LOGS_PIPELINES: '/logs/pipelines',
|
||||
|
||||
@@ -8,6 +8,7 @@ import cx from 'classnames';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
import { initialQueryAIWithType } from 'constants/queryBuilder';
|
||||
@@ -94,6 +95,13 @@ function Explorer(): JSX.Element {
|
||||
const [warning, setWarning] = useState<Warning | undefined>();
|
||||
const [isOpen, setOpen] = useState<boolean>(true);
|
||||
|
||||
const { startUnixMilli, endUnixMilli } = useSignalFieldApis();
|
||||
// existingQuery is left unset so related values auto-extract from the current query
|
||||
const quickFiltersFieldApis = useMemo(
|
||||
() => ({ startUnixMilli, endUnixMilli }),
|
||||
[startUnixMilli, endUnixMilli],
|
||||
);
|
||||
|
||||
const defaultQuery = useMemo(
|
||||
(): Query =>
|
||||
updateAllQueriesOperators(
|
||||
@@ -187,8 +195,9 @@ function Explorer(): JSX.Element {
|
||||
<Card className="filter" hidden={!isOpen}>
|
||||
<QuickFilters
|
||||
className="qf-traces-explorer"
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
signal={SignalType.TRACES}
|
||||
source={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
signal={SignalType.AI_OBSERVABILITY}
|
||||
useFieldApis={quickFiltersFieldApis}
|
||||
handleFilterVisibilityChange={(): void => {
|
||||
setOpen(!isOpen);
|
||||
}}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Button, Flex } from 'antd';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import Controls from 'container/Controls';
|
||||
import Download from 'container/Download/Download';
|
||||
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { OrderPreferenceItems } from 'pages/Logs/config';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
GET_NEXT_LOG_LINES,
|
||||
GET_PREVIOUS_LOG_LINES,
|
||||
RESET_ID_START_AND_END,
|
||||
SET_LOG_LINES_PER_PAGE,
|
||||
} from 'types/actions/logs';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { Container } from './styles';
|
||||
import { SkipBack } from '@signozhq/icons';
|
||||
|
||||
function LogControls(): JSX.Element | null {
|
||||
const {
|
||||
logLinesPerPage,
|
||||
liveTail,
|
||||
isLoading: isLogsLoading,
|
||||
isLoadingAggregate,
|
||||
logs,
|
||||
order,
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const handleLogLinesPerPageChange = (e: Pagination['limit']): void => {
|
||||
dispatch({
|
||||
type: SET_LOG_LINES_PER_PAGE,
|
||||
payload: {
|
||||
logsLinesPerPage: e,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleGoToLatest = (): void => {
|
||||
const { maxTime, minTime } = getMinMaxForSelectedTime(
|
||||
globalTime.selectedTime,
|
||||
globalTime.minTime,
|
||||
globalTime.maxTime,
|
||||
);
|
||||
|
||||
const updatedGlobalTime = getGlobalTime(globalTime.selectedTime, {
|
||||
maxTime,
|
||||
minTime,
|
||||
});
|
||||
|
||||
if (updatedGlobalTime) {
|
||||
dispatch({
|
||||
type: RESET_ID_START_AND_END,
|
||||
payload: updatedGlobalTime,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigatePrevious = (): void => {
|
||||
dispatch({
|
||||
type: GET_PREVIOUS_LOG_LINES,
|
||||
});
|
||||
};
|
||||
|
||||
const handleNavigateNext = (): void => {
|
||||
dispatch({
|
||||
type: GET_NEXT_LOG_LINES,
|
||||
});
|
||||
};
|
||||
|
||||
const flattenLogData = useMemo(
|
||||
() =>
|
||||
logs.map((log) => {
|
||||
const timestamp =
|
||||
typeof log.timestamp === 'string'
|
||||
? dayjs(log.timestamp).format(DATE_TIME_FORMATS.ISO_DATETIME_MS)
|
||||
: dayjs(log.timestamp / 1e6).format(DATE_TIME_FORMATS.ISO_DATETIME_MS);
|
||||
|
||||
return FlatLogData({
|
||||
...log,
|
||||
timestamp,
|
||||
});
|
||||
}),
|
||||
[logs],
|
||||
);
|
||||
|
||||
const isLoading = isLogsLoading || isLoadingAggregate;
|
||||
|
||||
if (liveTail !== 'STOPPED') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Download data={flattenLogData} isLoading={isLoading} fileName="log_data" />
|
||||
<Button
|
||||
loading={isLoading}
|
||||
size="small"
|
||||
type="link"
|
||||
disabled={order === OrderPreferenceItems.ASC}
|
||||
onClick={handleGoToLatest}
|
||||
>
|
||||
<Flex align="center" gap="4px">
|
||||
<SkipBack size="md" /> Go to latest
|
||||
</Flex>
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Controls
|
||||
isLoading={isLoading}
|
||||
totalCount={logs.length}
|
||||
countPerPage={logLinesPerPage}
|
||||
handleNavigatePrevious={handleNavigatePrevious}
|
||||
handleNavigateNext={handleNavigateNext}
|
||||
handleCountItemsPerPageChange={handleLogLinesPerPageChange}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LogControls);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Button } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
export const DownloadLogButton = styled(Button)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { Link, Pin } from '@signozhq/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
@@ -14,23 +12,19 @@ import { ResizeTable } from 'components/ResizeTable';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { FontSize, OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import history from 'lib/history';
|
||||
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
|
||||
import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import AppActions from 'types/actions';
|
||||
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { ActionItemProps } from './ActionItem';
|
||||
import { RESTRICTED_SELECTED_FIELDS } from './config';
|
||||
import FieldRenderer from './FieldRenderer';
|
||||
import TableViewActions from './TableView/TableViewActions';
|
||||
import {
|
||||
@@ -65,7 +59,6 @@ function TableView({
|
||||
listViewPanelSelectedFields,
|
||||
handleChangeSelectedView,
|
||||
}: Props): JSX.Element | null {
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
|
||||
const [isfilterOutLoading, setIsFilterOutLoading] = useState<boolean>(false);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
@@ -185,11 +178,6 @@ function TableView({
|
||||
const spanId = flattenLogData?.span_id;
|
||||
|
||||
if (traceId) {
|
||||
dispatch({
|
||||
type: SET_DETAILED_LOG_DATA,
|
||||
payload: null,
|
||||
});
|
||||
|
||||
const basePath = generatePath(ROUTES.TRACE_DETAIL, {
|
||||
id: traceId,
|
||||
});
|
||||
|
||||
@@ -10,7 +10,6 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { RESTRICTED_SELECTED_FIELDS } from '../config';
|
||||
import { DataType } from '../TableView';
|
||||
import {
|
||||
filterKeyForField,
|
||||
@@ -141,10 +141,9 @@ export default function TableViewActions(
|
||||
const { stagedQuery, updateQueriesData } = useQueryBuilder();
|
||||
const { dataType, logType: fieldType } = getFieldAttributes(record.field);
|
||||
|
||||
// there is no option for where clause in old logs explorer and live logs page or infra monitoring
|
||||
const isOldLogsExplorerOrLiveLogsPage = useMemo(
|
||||
// there is no option for where clause in live logs page or infra monitoring
|
||||
const isLiveLogsOrInfraPage = useMemo(
|
||||
() =>
|
||||
pathname === ROUTES.OLD_LOGS_EXPLORER ||
|
||||
pathname === ROUTES.LIVE_LOGS ||
|
||||
pathname === ROUTES.INFRASTRUCTURE_MONITORING_HOSTS ||
|
||||
pathname === ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
|
||||
@@ -400,7 +399,7 @@ export default function TableViewActions(
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
{!isOldLogsExplorerOrLiveLogsPage && (
|
||||
{!isLiveLogsOrInfraPage && (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
@@ -487,7 +486,7 @@ export default function TableViewActions(
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
{!isOldLogsExplorerOrLiveLogsPage && (
|
||||
{!isLiveLogsOrInfraPage && (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
|
||||
import { RESTRICTED_SELECTED_FIELDS } from '../../config';
|
||||
import TableViewActions from '../TableViewActions';
|
||||
import useAsyncJSONProcessing from '../useAsyncJSONProcessing';
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
|
||||
|
||||
// Fields that can be filtered on but not grouped by in the log details view.
|
||||
export const RESTRICTED_GROUP_BY_FIELDS = ['body', 'trace_id'];
|
||||
|
||||
export const typeToArrayTypeMapper: { [key in DataTypes]: DataTypes } = {
|
||||
[DataTypes.String]: DataTypes.ArrayString,
|
||||
[DataTypes.Float64]: DataTypes.ArrayFloat64,
|
||||
|
||||
@@ -61,8 +61,7 @@ export function useLogAttributeActions({
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
|
||||
?.active || false;
|
||||
|
||||
const isOldExplorerOrLive =
|
||||
pathname === ROUTES.OLD_LOGS_EXPLORER || pathname === ROUTES.LIVE_LOGS;
|
||||
const isLiveLogs = pathname === ROUTES.LIVE_LOGS;
|
||||
|
||||
const filterFor = useCallback(
|
||||
(context: FieldContext, isFilterIn: boolean): void => {
|
||||
@@ -221,7 +220,7 @@ export function useLogAttributeActions({
|
||||
!handleChangeSelectedView ||
|
||||
!buildLogFilterTarget(fieldKeyPath, undefined, isBodyJsonQueryEnabled)
|
||||
.groupBySupported ||
|
||||
isOldExplorerOrLive,
|
||||
isLiveLogs,
|
||||
},
|
||||
{
|
||||
key: LogDetailsAction.REPLACE_FILTER,
|
||||
@@ -229,9 +228,7 @@ export function useLogAttributeActions({
|
||||
icon: <RefreshCw size={12} />,
|
||||
onClick: replaceFilter,
|
||||
shouldHide: (_key, fieldKeyPath): boolean =>
|
||||
!handleChangeSelectedView ||
|
||||
isRestricted(fieldKeyPath) ||
|
||||
isOldExplorerOrLive,
|
||||
!handleChangeSelectedView || isRestricted(fieldKeyPath) || isLiveLogs,
|
||||
},
|
||||
];
|
||||
}, [
|
||||
@@ -239,7 +236,7 @@ export function useLogAttributeActions({
|
||||
groupBy,
|
||||
replaceFilter,
|
||||
isBodyJsonQueryEnabled,
|
||||
isOldExplorerOrLive,
|
||||
isLiveLogs,
|
||||
handleChangeSelectedView,
|
||||
onApplyLogFilter,
|
||||
]);
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import { memo, useCallback } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog';
|
||||
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
|
||||
import getStep from 'lib/getStep';
|
||||
import { getIdConditions } from 'pages/Logs/utils';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { getLogs } from 'store/actions/logs/getLogs';
|
||||
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
SET_DETAILED_LOG_DATA,
|
||||
SET_SEARCH_QUERY_STRING,
|
||||
TOGGLE_LIVE_TAIL,
|
||||
} from 'types/actions/logs';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
type LogDetailedViewProps = {
|
||||
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
|
||||
getLogsAggregate: (
|
||||
props: Parameters<typeof getLogsAggregate>[0],
|
||||
) => ReturnType<typeof getLogsAggregate>;
|
||||
};
|
||||
|
||||
function LogDetailedView({
|
||||
getLogs,
|
||||
getLogsAggregate,
|
||||
}: LogDetailedViewProps): JSX.Element {
|
||||
const history = useHistory();
|
||||
const {
|
||||
detailedLog,
|
||||
searchFilter: { queryString },
|
||||
logLinesPerPage,
|
||||
idStart,
|
||||
liveTail,
|
||||
idEnd,
|
||||
order,
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const onDrawerClose = (): void => {
|
||||
dispatch({
|
||||
type: SET_DETAILED_LOG_DATA,
|
||||
payload: null,
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddToQuery = useCallback(
|
||||
(fieldKey: string, fieldValue: string, operator: string) => {
|
||||
const newOperator = getOldLogsOperatorFromNew(operator);
|
||||
const updatedQueryString = getGeneratedFilterQueryString(
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
newOperator,
|
||||
queryString,
|
||||
);
|
||||
|
||||
history.replace(`${ROUTES.OLD_LOGS_EXPLORER}?q=${updatedQueryString}`);
|
||||
},
|
||||
[history, queryString],
|
||||
);
|
||||
|
||||
const handleClickActionItem = useCallback(
|
||||
(fieldKey: string, fieldValue: string, operator: string): void => {
|
||||
const newOperator = getOldLogsOperatorFromNew(operator);
|
||||
const updatedQueryString = getGeneratedFilterQueryString(
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
newOperator,
|
||||
queryString,
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_STRING,
|
||||
payload: {
|
||||
searchQueryString: updatedQueryString,
|
||||
},
|
||||
});
|
||||
|
||||
if (liveTail === 'STOPPED') {
|
||||
getLogs({
|
||||
q: updatedQueryString,
|
||||
limit: logLinesPerPage,
|
||||
orderBy: 'timestamp',
|
||||
order,
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
...getIdConditions(idStart, idEnd, order),
|
||||
});
|
||||
getLogsAggregate({
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
step: getStep({
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
inputFormat: 'ns',
|
||||
}),
|
||||
q: updatedQueryString,
|
||||
});
|
||||
} else if (liveTail === 'PLAYING') {
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: 'PAUSED',
|
||||
});
|
||||
setTimeout(
|
||||
() =>
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: liveTail,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
dispatch,
|
||||
getLogs,
|
||||
getLogsAggregate,
|
||||
idEnd,
|
||||
idStart,
|
||||
liveTail,
|
||||
logLinesPerPage,
|
||||
maxTime,
|
||||
minTime,
|
||||
order,
|
||||
queryString,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={detailedLog}
|
||||
onClose={onDrawerClose}
|
||||
onAddToQuery={handleAddToQuery}
|
||||
onClickActionItem={handleClickActionItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
getLogs: (props: Parameters<typeof getLogs>[0]) => (dispatch: never) => void;
|
||||
getLogsAggregate: (
|
||||
props: Parameters<typeof getLogsAggregate>[0],
|
||||
) => (dispatch: never) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getLogs: bindActionCreators(getLogs, dispatch),
|
||||
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(memo(LogDetailedView as any));
|
||||
@@ -5,10 +5,6 @@ import {
|
||||
QUERY_BUILDER_FUNCTIONS,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { OPERATORS as QUERY_BUILDER_OPERATORS } from 'constants/queryBuilder';
|
||||
import {
|
||||
RESTRICTED_GROUP_BY_FIELDS,
|
||||
RESTRICTED_SELECTED_FIELDS,
|
||||
} from 'container/LogsFilters/config';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||
@@ -18,6 +14,10 @@ import {
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
RESTRICTED_GROUP_BY_FIELDS,
|
||||
RESTRICTED_SELECTED_FIELDS,
|
||||
} from './config';
|
||||
import { LogAttributeBucket } from './constants';
|
||||
import { generateFieldKeyForArray, getDataTypes } from './utils';
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
export const TIME_PICKER_OPTIONS = [
|
||||
{
|
||||
value: 5,
|
||||
label: '5m',
|
||||
},
|
||||
{
|
||||
value: 15,
|
||||
label: '15m',
|
||||
},
|
||||
{
|
||||
value: 30,
|
||||
label: '30m',
|
||||
},
|
||||
{
|
||||
value: 60,
|
||||
label: '1hr',
|
||||
},
|
||||
{
|
||||
value: 360,
|
||||
label: '6hrs',
|
||||
},
|
||||
{
|
||||
value: 720,
|
||||
label: '12hrs',
|
||||
},
|
||||
];
|
||||
@@ -1,270 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { green } from '@ant-design/colors';
|
||||
import { Pause, Play, EllipsisVertical } from '@signozhq/icons';
|
||||
import { Button, Flex, Popover, Select, Space } from 'antd';
|
||||
import { LiveTail } from 'api/logs/livetail';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import getStep from 'lib/getStep';
|
||||
import { throttle } from 'lodash-es';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_AUTO_REFRESH_DISABLED } from 'types/actions/globalTime';
|
||||
import {
|
||||
FLUSH_LOGS,
|
||||
PUSH_LIVE_TAIL_EVENT,
|
||||
SET_LIVE_TAIL_START_TIME,
|
||||
SET_LOADING,
|
||||
TOGGLE_LIVE_TAIL,
|
||||
} from 'types/actions/logs';
|
||||
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { TIME_PICKER_OPTIONS } from './config';
|
||||
import { StopContainer, TimePickerCard, TimePickerSelect } from './styles';
|
||||
|
||||
function LogLiveTail({ getLogsAggregate }: Props): JSX.Element {
|
||||
const {
|
||||
liveTail,
|
||||
searchFilter: { queryString },
|
||||
liveTailStartRange,
|
||||
logs,
|
||||
idEnd,
|
||||
idStart,
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const { selectedAutoRefreshInterval } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const handleLiveTail = (toggleState: TLogsLiveTailState): void => {
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: toggleState,
|
||||
});
|
||||
dispatch({
|
||||
type: UPDATE_AUTO_REFRESH_DISABLED,
|
||||
payload: toggleState === 'PLAYING',
|
||||
});
|
||||
};
|
||||
|
||||
const batchedEventsRef = useRef<ILog[]>([]);
|
||||
|
||||
const pushLiveLog = useCallback(() => {
|
||||
dispatch({
|
||||
type: PUSH_LIVE_TAIL_EVENT,
|
||||
payload: batchedEventsRef.current.reverse(),
|
||||
});
|
||||
batchedEventsRef.current = [];
|
||||
}, [dispatch]);
|
||||
|
||||
const pushLiveLogThrottled = useMemo(
|
||||
() => throttle(pushLiveLog, 1000),
|
||||
[pushLiveLog],
|
||||
);
|
||||
|
||||
const batchLiveLog = useCallback(
|
||||
(e: { data: string }): void => {
|
||||
batchedEventsRef.current.push(JSON.parse(e.data as string) as never);
|
||||
pushLiveLogThrottled();
|
||||
},
|
||||
[pushLiveLogThrottled],
|
||||
);
|
||||
|
||||
const firstLogsId = useMemo(() => logs[0]?.id, [logs]);
|
||||
|
||||
// This ref depicts thats whether the live tail is played from paused state or not.
|
||||
const liveTailSourceRef = useRef<EventSource>();
|
||||
|
||||
useEffect(() => {
|
||||
if (liveTail === 'PLAYING') {
|
||||
const timeStamp = dayjs().subtract(liveTailStartRange, 'minute').valueOf();
|
||||
const queryParams = new URLSearchParams({
|
||||
...(queryString ? { q: queryString } : {}),
|
||||
timestampStart: (timeStamp * 1e6) as never,
|
||||
...(liveTailSourceRef.current && firstLogsId
|
||||
? {
|
||||
idGt: firstLogsId,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (liveTailSourceRef.current) {
|
||||
liveTailSourceRef.current.close();
|
||||
}
|
||||
|
||||
const source = LiveTail(queryParams.toString());
|
||||
liveTailSourceRef.current = source;
|
||||
source.onmessage = function connectionMessage(e): void {
|
||||
batchLiveLog(e);
|
||||
};
|
||||
source.onerror = function connectionError(event: unknown): void {
|
||||
console.error(event);
|
||||
source.close();
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: 'STOPPED',
|
||||
});
|
||||
dispatch({
|
||||
type: SET_LOADING,
|
||||
payload: false,
|
||||
});
|
||||
notifications.error({
|
||||
message: 'Live tail stopped due to some error.',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
if (liveTail === 'STOPPED') {
|
||||
liveTailSourceRef.current = undefined;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [liveTail, queryString, notifications, dispatch]);
|
||||
|
||||
const handleLiveTailStart = (): void => {
|
||||
handleLiveTail('PLAYING');
|
||||
const startTime =
|
||||
dayjs().subtract(liveTailStartRange, 'minute').valueOf() * 1e6;
|
||||
|
||||
const endTime = dayjs().valueOf() * 1e6;
|
||||
|
||||
getLogsAggregate({
|
||||
timestampStart: startTime,
|
||||
timestampEnd: endTime,
|
||||
step: getStep({
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
inputFormat: 'ns',
|
||||
}),
|
||||
q: queryString,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
});
|
||||
|
||||
if (!liveTailSourceRef.current) {
|
||||
dispatch({
|
||||
type: FLUSH_LOGS,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const OptionsPopOverContent = useMemo(
|
||||
() => (
|
||||
<TimePickerSelect
|
||||
getPopupContainer={popupContainer}
|
||||
disabled={liveTail === 'PLAYING'}
|
||||
value={liveTailStartRange}
|
||||
onChange={(value): void => {
|
||||
if (typeof value === 'number') {
|
||||
dispatch({
|
||||
type: SET_LIVE_TAIL_START_TIME,
|
||||
payload: value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{TIME_PICKER_OPTIONS.map((optionData) => (
|
||||
<Select.Option key={optionData.label} value={optionData.value}>
|
||||
Last {optionData.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</TimePickerSelect>
|
||||
),
|
||||
[dispatch, liveTail, liveTailStartRange],
|
||||
);
|
||||
|
||||
const isDisabled = useMemo(
|
||||
() => selectedAutoRefreshInterval?.length > 0,
|
||||
[selectedAutoRefreshInterval],
|
||||
);
|
||||
|
||||
const onLiveTailStop = (): void => {
|
||||
handleLiveTail('STOPPED');
|
||||
dispatch({
|
||||
type: UPDATE_AUTO_REFRESH_DISABLED,
|
||||
payload: false,
|
||||
});
|
||||
dispatch({
|
||||
type: SET_LOADING,
|
||||
payload: false,
|
||||
});
|
||||
if (liveTailSourceRef.current) {
|
||||
liveTailSourceRef.current.close();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TimePickerCard>
|
||||
<Space size={0} align="center">
|
||||
{liveTail === 'PLAYING' ? (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onLiveTailStop}
|
||||
title="Pause live tail"
|
||||
style={{ background: green[6] }}
|
||||
>
|
||||
<Flex align="center" gap={4}>
|
||||
<span>Pause</span>
|
||||
<Pause size="md" />
|
||||
</Flex>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleLiveTailStart}
|
||||
title="Start live tail"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<Flex align="center" gap={4}>
|
||||
Go Live <Play size="md" />
|
||||
</Flex>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{liveTail !== 'STOPPED' && (
|
||||
<Button type="dashed" onClick={onLiveTailStop} title="Exit live tail">
|
||||
<StopContainer isDarkMode={isDarkMode} />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Popover
|
||||
getPopupContainer={popupContainer}
|
||||
placement="bottomRight"
|
||||
title="Select Live Tail Timing"
|
||||
trigger="click"
|
||||
content={OptionsPopOverContent}
|
||||
>
|
||||
<EllipsisVertical size="lg" />
|
||||
</Popover>
|
||||
</Space>
|
||||
</TimePickerCard>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
getLogsAggregate: typeof getLogsAggregate;
|
||||
}
|
||||
|
||||
type Props = DispatchProps;
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(LogLiveTail);
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Card, Select } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const TimePickerCard = styled(Card)`
|
||||
.ant-card-body {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TimePickerSelect = styled(Select)`
|
||||
min-width: 100px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const StopContainer = styled.div<Props>`
|
||||
height: 0.8rem;
|
||||
width: 0.8rem;
|
||||
border-radius: 0.1rem;
|
||||
background-color: ${({ isDarkMode }): string =>
|
||||
isDarkMode ? '#fff' : '#000'};
|
||||
`;
|
||||
@@ -1,95 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { blue } from '@ant-design/colors';
|
||||
import Graph from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import dayjs from 'dayjs';
|
||||
import useInterval from 'hooks/useInterval';
|
||||
import getStep from 'lib/getStep';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { Container } from './styles';
|
||||
|
||||
function LogsAggregate({ getLogsAggregate }: DispatchProps): JSX.Element {
|
||||
const {
|
||||
searchFilter: { queryString },
|
||||
idEnd,
|
||||
idStart,
|
||||
isLoadingAggregate,
|
||||
logsAggregate,
|
||||
liveTail,
|
||||
liveTailStartRange,
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
const startTime =
|
||||
dayjs().subtract(liveTailStartRange, 'minute').valueOf() * 1e6;
|
||||
|
||||
const endTime = dayjs().valueOf() * 1e6;
|
||||
|
||||
getLogsAggregate({
|
||||
timestampStart: startTime,
|
||||
timestampEnd: endTime,
|
||||
step: getStep({
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
inputFormat: 'ns',
|
||||
}),
|
||||
q: queryString,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
});
|
||||
},
|
||||
60000,
|
||||
liveTail === 'PLAYING',
|
||||
);
|
||||
|
||||
const graphData = useMemo(
|
||||
() => ({
|
||||
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
|
||||
datasets: [
|
||||
{
|
||||
data: logsAggregate.map((s) => s.value),
|
||||
backgroundColor: blue[4],
|
||||
},
|
||||
],
|
||||
}),
|
||||
[logsAggregate],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{isLoadingAggregate ? (
|
||||
<Spinner size="default" height="100%" />
|
||||
) : (
|
||||
<Graph
|
||||
name="usage"
|
||||
data={graphData}
|
||||
type="bar"
|
||||
containerHeight="100%"
|
||||
animate
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
getLogsAggregate: typeof getLogsAggregate;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(LogsAggregate);
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Card } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled(Card)`
|
||||
position: relative;
|
||||
margin: 0.5rem 0;
|
||||
.ant-card-body {
|
||||
height: 20vh;
|
||||
min-height: 200px;
|
||||
}
|
||||
`;
|
||||
@@ -1,93 +0,0 @@
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { Button, Popover, Spin } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import {
|
||||
IField,
|
||||
IInterestingFields,
|
||||
ISelectedFields,
|
||||
} from 'types/api/logs/fields';
|
||||
|
||||
import { ICON_STYLE } from './config';
|
||||
import { Field } from './styles';
|
||||
|
||||
function FieldItem({
|
||||
name,
|
||||
buttonIcon,
|
||||
buttonOnClick,
|
||||
fieldData,
|
||||
fieldIndex,
|
||||
isLoading,
|
||||
iconHoverText,
|
||||
}: FieldItemProps): JSX.Element {
|
||||
const [isHovered, setIsHovered] = useState<boolean>(false);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const onClickHandler = useCallback(() => {
|
||||
if (!isLoading && buttonOnClick) {
|
||||
buttonOnClick({ fieldData, fieldIndex });
|
||||
}
|
||||
}, [buttonOnClick, fieldData, fieldIndex, isLoading]);
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Spin
|
||||
spinning
|
||||
size="small"
|
||||
indicator={<Loader className="animate-spin" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isHovered) {
|
||||
return (
|
||||
<Popover content={<Typography>{iconHoverText}</Typography>}>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={buttonIcon}
|
||||
onClick={onClickHandler}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [buttonIcon, iconHoverText, isHovered, isLoading, onClickHandler]);
|
||||
|
||||
const onMouseHoverHandler = useCallback(
|
||||
(value: boolean) => (): void => {
|
||||
setIsHovered(value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Field
|
||||
onMouseEnter={onMouseHoverHandler(true)}
|
||||
onMouseLeave={onMouseHoverHandler(false)}
|
||||
isDarkMode={isDarkMode}
|
||||
>
|
||||
<Typography style={ICON_STYLE.PLUS}>{name}</Typography>
|
||||
|
||||
{renderContent}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
interface FieldItemProps {
|
||||
name: string;
|
||||
buttonIcon: ReactNode;
|
||||
buttonOnClick: (props: {
|
||||
fieldData: IInterestingFields | ISelectedFields;
|
||||
fieldIndex: number;
|
||||
}) => void;
|
||||
fieldData: IField;
|
||||
fieldIndex: number;
|
||||
isLoading: boolean;
|
||||
iconHoverText: string;
|
||||
}
|
||||
|
||||
export default FieldItem;
|
||||
@@ -1,11 +0,0 @@
|
||||
import { blue, red } from '@ant-design/colors';
|
||||
|
||||
export const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
|
||||
|
||||
// Fields that can be filtered on but not grouped by in the log details view.
|
||||
export const RESTRICTED_GROUP_BY_FIELDS = ['body', 'trace_id'];
|
||||
|
||||
export const ICON_STYLE = {
|
||||
PLUS: { color: blue[5] },
|
||||
CLOSE: { color: red[5] },
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
import { ChangeEvent, useCallback, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { CirclePlus, X } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Col } from 'antd';
|
||||
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { ICON_STYLE, RESTRICTED_SELECTED_FIELDS } from './config';
|
||||
import FieldItem from './FieldItem';
|
||||
import { CategoryContainer, FieldContainer } from './styles';
|
||||
import { IHandleInterestProps, IHandleRemoveInterestProps } from './types';
|
||||
import { onHandleAddInterest, onHandleRemoveInterest } from './utils';
|
||||
|
||||
function LogsFilters(): JSX.Element {
|
||||
const {
|
||||
fields: { interesting, selected },
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
|
||||
const [selectedFieldLoading, setSelectedFieldLoading] = useState<number[]>([]);
|
||||
const [interestingFieldLoading, setInterestingFieldLoading] = useState<
|
||||
number[]
|
||||
>([]);
|
||||
|
||||
const [filterValuesInput, setFilterValuesInput] = useState('');
|
||||
const handleSearch = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setFilterValuesInput((e.target as HTMLInputElement).value);
|
||||
};
|
||||
|
||||
const onHandleAddSelectedToInteresting = useCallback(
|
||||
({ fieldData, fieldIndex }: IHandleInterestProps) =>
|
||||
(): Promise<void> =>
|
||||
onHandleAddInterest({
|
||||
fieldData,
|
||||
fieldIndex,
|
||||
interesting,
|
||||
interestingFieldLoading,
|
||||
setInterestingFieldLoading,
|
||||
selected,
|
||||
}),
|
||||
[interesting, interestingFieldLoading, selected],
|
||||
);
|
||||
|
||||
const onHandleRemoveSelected = useCallback(
|
||||
({ fieldData, fieldIndex }: IHandleRemoveInterestProps) =>
|
||||
(): Promise<void> =>
|
||||
onHandleRemoveInterest({
|
||||
fieldData,
|
||||
fieldIndex,
|
||||
interesting,
|
||||
interestingFieldLoading,
|
||||
selected,
|
||||
setSelectedFieldLoading,
|
||||
}),
|
||||
[interesting, interestingFieldLoading, selected, setSelectedFieldLoading],
|
||||
);
|
||||
|
||||
return (
|
||||
<Col flex="250px">
|
||||
<Input
|
||||
placeholder="Filter Values"
|
||||
onInput={handleSearch}
|
||||
value={filterValuesInput}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
|
||||
<CategoryContainer>
|
||||
<CategoryHeading>SELECTED FIELDS</CategoryHeading>
|
||||
<FieldContainer>
|
||||
{selected
|
||||
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
|
||||
.filter((field) => RESTRICTED_SELECTED_FIELDS.indexOf(field.name) === -1)
|
||||
.map((field, idx) => (
|
||||
<FieldItem
|
||||
key={`${JSON.stringify(field)}`}
|
||||
name={field.name}
|
||||
fieldData={field}
|
||||
fieldIndex={idx}
|
||||
buttonIcon={<X style={ICON_STYLE.CLOSE} size="md" />}
|
||||
buttonOnClick={onHandleRemoveSelected({
|
||||
fieldData: field,
|
||||
fieldIndex: idx,
|
||||
})}
|
||||
isLoading={selectedFieldLoading.includes(idx)}
|
||||
iconHoverText="Remove from Selected Fields"
|
||||
/>
|
||||
))}
|
||||
</FieldContainer>
|
||||
</CategoryContainer>
|
||||
<CategoryContainer>
|
||||
<CategoryHeading>INTERESTING FIELDS</CategoryHeading>
|
||||
<FieldContainer>
|
||||
{interesting
|
||||
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
|
||||
.map((field, idx) => (
|
||||
<FieldItem
|
||||
key={`${JSON.stringify(field)}`}
|
||||
name={field.name}
|
||||
fieldData={field}
|
||||
fieldIndex={idx}
|
||||
buttonIcon={<CirclePlus style={ICON_STYLE.PLUS} size="md" />}
|
||||
buttonOnClick={onHandleAddSelectedToInteresting({
|
||||
fieldData: field,
|
||||
fieldIndex: idx,
|
||||
})}
|
||||
isLoading={interestingFieldLoading.includes(idx)}
|
||||
iconHoverText="Add to Selected Fields"
|
||||
/>
|
||||
))}
|
||||
</FieldContainer>
|
||||
</CategoryContainer>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogsFilters;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { blue, grey } from '@ant-design/colors';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const CategoryContainer = styled.div`
|
||||
margin: 1rem 0;
|
||||
padding-left: 0.2rem;
|
||||
`;
|
||||
|
||||
export const FieldContainer = styled(Typography.Text)`
|
||||
margin: 0.2rem 0;
|
||||
color: ${blue[4]};
|
||||
`;
|
||||
|
||||
export const Field = styled.div<{ isDarkMode: boolean }>`
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
background: ${({ isDarkMode }): string => (isDarkMode ? grey[7] : '#ddd')};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ExtractField = styled(Typography.Text)`
|
||||
color: ${blue[4]};
|
||||
`;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { SetStateAction } from 'react';
|
||||
import {
|
||||
IField,
|
||||
IInterestingFields,
|
||||
ISelectedFields,
|
||||
} from 'types/api/logs/fields';
|
||||
|
||||
type SetLoading = (value: SetStateAction<number[]>) => void;
|
||||
|
||||
export type IHandleInterestProps = {
|
||||
fieldData: IInterestingFields;
|
||||
fieldIndex: number;
|
||||
};
|
||||
|
||||
export type IHandleRemoveInterestProps = {
|
||||
fieldData: ISelectedFields;
|
||||
fieldIndex: number;
|
||||
};
|
||||
|
||||
export interface OnHandleAddInterestProps {
|
||||
setInterestingFieldLoading: SetLoading;
|
||||
fieldIndex: number;
|
||||
fieldData: ISelectedFields;
|
||||
interesting: IField[];
|
||||
interestingFieldLoading: number[];
|
||||
selected: IField[];
|
||||
}
|
||||
|
||||
export interface OnHandleRemoveInterestProps {
|
||||
setSelectedFieldLoading: SetLoading;
|
||||
selected: IField[];
|
||||
interesting: IField[];
|
||||
interestingFieldLoading: number[];
|
||||
fieldData: IInterestingFields;
|
||||
fieldIndex: number;
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { message } from 'antd';
|
||||
import addToSelectedFields from 'api/logs/AddToSelectedField';
|
||||
import removeSelectedField from 'api/logs/RemoveFromSelectedField';
|
||||
import store from 'store';
|
||||
import {
|
||||
UPDATE_INTERESTING_FIELDS,
|
||||
UPDATE_SELECTED_FIELDS,
|
||||
} from 'types/actions/logs';
|
||||
import { ErrorResponse } from 'types/api';
|
||||
|
||||
import { RESTRICTED_SELECTED_FIELDS } from './config';
|
||||
import { OnHandleAddInterestProps, OnHandleRemoveInterestProps } from './types';
|
||||
|
||||
export const onHandleAddInterest = async ({
|
||||
setInterestingFieldLoading,
|
||||
fieldIndex,
|
||||
fieldData,
|
||||
interesting,
|
||||
interestingFieldLoading,
|
||||
selected,
|
||||
}: OnHandleAddInterestProps): Promise<void> => {
|
||||
const { dispatch } = store;
|
||||
|
||||
setInterestingFieldLoading((prevState: number[]) => {
|
||||
prevState.push(fieldIndex);
|
||||
return [...prevState];
|
||||
});
|
||||
|
||||
try {
|
||||
await addToSelectedFields({
|
||||
...fieldData,
|
||||
selected: true,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_INTERESTING_FIELDS,
|
||||
payload: {
|
||||
field: interesting.filter((e) => e.name !== fieldData.name),
|
||||
type: 'selected',
|
||||
},
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_SELECTED_FIELDS,
|
||||
payload: {
|
||||
field: [...selected, fieldData],
|
||||
type: 'selected',
|
||||
},
|
||||
});
|
||||
} catch (errRes) {
|
||||
message.error((errRes as ErrorResponse)?.error);
|
||||
} finally {
|
||||
setInterestingFieldLoading(
|
||||
interestingFieldLoading.filter((e) => e !== fieldIndex),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const onHandleRemoveInterest = async ({
|
||||
setSelectedFieldLoading,
|
||||
selected,
|
||||
interesting,
|
||||
interestingFieldLoading,
|
||||
fieldData,
|
||||
fieldIndex,
|
||||
}: OnHandleRemoveInterestProps): Promise<void> => {
|
||||
if (RESTRICTED_SELECTED_FIELDS.includes(fieldData.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { dispatch } = store;
|
||||
|
||||
setSelectedFieldLoading((prevState) => {
|
||||
prevState.push(fieldIndex);
|
||||
return [...prevState];
|
||||
});
|
||||
|
||||
try {
|
||||
await removeSelectedField({
|
||||
...fieldData,
|
||||
selected: false,
|
||||
});
|
||||
dispatch({
|
||||
type: UPDATE_SELECTED_FIELDS,
|
||||
payload: {
|
||||
field: selected.filter((e) => e.name !== fieldData.name),
|
||||
type: 'selected',
|
||||
},
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_INTERESTING_FIELDS,
|
||||
payload: {
|
||||
field: [...interesting, fieldData],
|
||||
type: 'interesting',
|
||||
},
|
||||
});
|
||||
} catch (errRes) {
|
||||
message.error((errRes as ErrorResponse)?.error);
|
||||
} finally {
|
||||
setSelectedFieldLoading(
|
||||
interestingFieldLoading.filter((e) => e !== fieldIndex),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Button, Row } from 'antd';
|
||||
|
||||
interface SearchFieldsActionBarProps {
|
||||
applyUpdate: VoidFunction;
|
||||
clearFilters: VoidFunction;
|
||||
}
|
||||
|
||||
export function SearchFieldsActionBar({
|
||||
applyUpdate,
|
||||
clearFilters,
|
||||
}: SearchFieldsActionBarProps): JSX.Element | null {
|
||||
return (
|
||||
<Row style={{ justifyContent: 'flex-end', paddingRight: '2.4rem' }}>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={clearFilters}
|
||||
style={{ marginRight: '1rem' }}
|
||||
>
|
||||
Clear Filter
|
||||
</Button>
|
||||
<Button type="primary" onClick={applyUpdate}>
|
||||
Apply
|
||||
</Button>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
export default SearchFieldsActionBar;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
interface FieldKeyProps {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
function FieldKey({ name, type }: FieldKeyProps): JSX.Element {
|
||||
return (
|
||||
<span style={{ margin: '0.25rem 0', display: 'flex', gap: '0.5rem' }}>
|
||||
<Typography.Text>{name}</Typography.Text>
|
||||
<Typography.Text color="muted" italic>
|
||||
{type}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default FieldKey;
|
||||
@@ -1,259 +0,0 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { SquareX, X } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Select } from 'antd';
|
||||
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||
import {
|
||||
ConditionalOperators,
|
||||
QueryOperatorsMultiVal,
|
||||
QueryOperatorsSingleVal,
|
||||
} from 'lib/logql/tokens';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import FieldKey from '../FieldKey';
|
||||
import { QueryFieldContainer } from '../styles';
|
||||
import { QueryFields } from '../utils';
|
||||
import { Container, QueryWrapper } from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
function QueryConditionField({
|
||||
query,
|
||||
queryIndex,
|
||||
onUpdate,
|
||||
}: QueryConditionFieldProps): JSX.Element {
|
||||
const allOptions = Object.values(ConditionalOperators);
|
||||
return (
|
||||
<Select
|
||||
defaultValue={
|
||||
(query as QueryFields).value &&
|
||||
(
|
||||
(query as QueryFields)
|
||||
?.value as unknown as QueryFields as unknown as string
|
||||
).toUpperCase()
|
||||
}
|
||||
onChange={(e): void => {
|
||||
onUpdate({ ...query, value: e }, queryIndex);
|
||||
}}
|
||||
>
|
||||
{allOptions.map((cond) => (
|
||||
<Option key={cond} value={cond} label={cond}>
|
||||
{cond}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
interface QueryFieldProps {
|
||||
query: Query;
|
||||
queryIndex: number;
|
||||
onUpdate: (query: Query, queryIndex: number) => void;
|
||||
onDelete: (queryIndex: number) => void;
|
||||
}
|
||||
function QueryField({
|
||||
query,
|
||||
queryIndex,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}: QueryFieldProps): JSX.Element | null {
|
||||
const [isDropDownOpen, setIsDropDownOpen] = useState(false);
|
||||
|
||||
const {
|
||||
fields: { selected },
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
const getFieldType = useCallback(
|
||||
(inputKey: string): string => {
|
||||
const selectedField = selected.find((field) => inputKey === field.name);
|
||||
if (selectedField) {
|
||||
return selectedField.type;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
[selected],
|
||||
);
|
||||
|
||||
const fieldType = useMemo(
|
||||
() => getFieldType(query[0].value as string),
|
||||
[getFieldType, query],
|
||||
);
|
||||
|
||||
const handleChange = (qIdx: number, value: string): void => {
|
||||
const updatedQuery = [...query];
|
||||
updatedQuery[qIdx].value = value || '';
|
||||
|
||||
if (qIdx === 1) {
|
||||
if (Object.values(QueryOperatorsMultiVal).includes(value)) {
|
||||
if (!Array.isArray(updatedQuery[2].value)) {
|
||||
updatedQuery[2].value = [];
|
||||
}
|
||||
} else if (
|
||||
Object.values(QueryOperatorsSingleVal).includes(value) &&
|
||||
Array.isArray(updatedQuery[2].value)
|
||||
) {
|
||||
updatedQuery[2].value = '';
|
||||
}
|
||||
}
|
||||
onUpdate(updatedQuery, queryIndex);
|
||||
};
|
||||
|
||||
const handleClear = (): void => {
|
||||
onDelete(queryIndex);
|
||||
};
|
||||
if (!Array.isArray(query)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryFieldContainer
|
||||
style={{ ...(queryIndex === 0 && { gridColumnStart: 2 }) }}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 100 }}>
|
||||
<FieldKey name={(query[0] && query[0].value) as string} type={fieldType} />
|
||||
</div>
|
||||
<Select
|
||||
defaultActiveFirstOption={false}
|
||||
placeholder="Select Operator"
|
||||
defaultValue={
|
||||
query[1] && query[1].value
|
||||
? (query[1].value as string).toUpperCase()
|
||||
: null
|
||||
}
|
||||
onChange={(e): void => handleChange(1, e)}
|
||||
style={{ minWidth: 150 }}
|
||||
>
|
||||
{Object.values({
|
||||
...QueryOperatorsMultiVal,
|
||||
...QueryOperatorsSingleVal,
|
||||
}).map((cond) => (
|
||||
<Option key={cond} value={cond} label={cond}>
|
||||
{cond}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<div style={{ flex: 2 }}>
|
||||
{Array.isArray(query[2].value) ||
|
||||
Object.values(QueryOperatorsMultiVal).some(
|
||||
(op) => op.toUpperCase() === (query[1].value as string)?.toUpperCase(),
|
||||
) ? (
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
open={isDropDownOpen}
|
||||
onChange={(e): void => handleChange(2, e as never)}
|
||||
defaultValue={(query[2] && query[2].value) || []}
|
||||
notFoundContent={null}
|
||||
onInputKeyDown={(): void => setIsDropDownOpen(true)}
|
||||
onSelect={(): void => setIsDropDownOpen(false)}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
onChange={(e): void => {
|
||||
handleChange(2, e.target.value);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
defaultValue={query[2] && query[2].value}
|
||||
value={query[2] && query[2].value}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
icon={<X size="md" />}
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={handleClear}
|
||||
/>
|
||||
</QueryFieldContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface QueryConditionFieldProps {
|
||||
query: QueryFields;
|
||||
queryIndex: number;
|
||||
onUpdate: (arg0: unknown, arg1: number) => void;
|
||||
}
|
||||
|
||||
export type Query = { value: string | string[]; type: string }[];
|
||||
|
||||
export interface QueryBuilderProps {
|
||||
keyPrefix: string;
|
||||
onDropDownToggleHandler: (value: boolean) => VoidFunction;
|
||||
fieldsQuery: QueryFields[][];
|
||||
setFieldsQuery: (q: QueryFields[][]) => void;
|
||||
syncKeyPrefix: () => void;
|
||||
}
|
||||
|
||||
function QueryBuilder({
|
||||
keyPrefix,
|
||||
fieldsQuery,
|
||||
setFieldsQuery,
|
||||
onDropDownToggleHandler,
|
||||
syncKeyPrefix,
|
||||
}: QueryBuilderProps): JSX.Element {
|
||||
const handleUpdate = (query: Query, queryIndex: number): void => {
|
||||
const updated = [...fieldsQuery];
|
||||
updated[queryIndex] = query as never; // parseQuery(query) as never;
|
||||
setFieldsQuery(updated);
|
||||
};
|
||||
|
||||
const handleDelete = (queryIndex: number): void => {
|
||||
const updated = [...fieldsQuery];
|
||||
if (queryIndex !== 0) {
|
||||
updated.splice(queryIndex - 1, 2);
|
||||
} else {
|
||||
updated.splice(queryIndex, 2);
|
||||
}
|
||||
|
||||
setFieldsQuery(updated);
|
||||
|
||||
// initiate re-render query panel
|
||||
syncKeyPrefix();
|
||||
};
|
||||
|
||||
const QueryUI = (
|
||||
fieldsQuery: QueryFields[][],
|
||||
): JSX.Element | JSX.Element[] => {
|
||||
const result: JSX.Element[] = [];
|
||||
fieldsQuery.forEach((query, idx) => {
|
||||
if (Array.isArray(query) && query.length > 1) {
|
||||
result.push(
|
||||
<QueryField
|
||||
key={keyPrefix}
|
||||
query={query}
|
||||
queryIndex={idx}
|
||||
onUpdate={handleUpdate}
|
||||
onDelete={handleDelete}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
result.push(
|
||||
<div key={keyPrefix}>
|
||||
<QueryConditionField
|
||||
query={Array.isArray(query) ? query[0] : query}
|
||||
queryIndex={idx}
|
||||
onUpdate={handleUpdate as never}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container isMargin={fieldsQuery.length === 0}>
|
||||
<CategoryHeading>LOG QUERY BUILDER</CategoryHeading>
|
||||
<SquareX onClick={onDropDownToggleHandler(false)} size="md" />
|
||||
</Container>
|
||||
|
||||
<QueryWrapper key={keyPrefix}>{QueryUI(fieldsQuery)}</QueryWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueryBuilder;
|
||||
@@ -1,17 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface Props {
|
||||
isMargin: boolean;
|
||||
}
|
||||
export const Container = styled.div<Props>`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-bottom: ${(props): string => (props.isMargin ? '2rem' : '0')};
|
||||
`;
|
||||
|
||||
export const QueryWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 80px 1fr;
|
||||
margin: 0.5rem 0px;
|
||||
`;
|
||||
@@ -1,62 +0,0 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Button } from 'antd';
|
||||
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||
import map from 'lodash-es/map';
|
||||
import { AppState } from 'store/reducers';
|
||||
// import { ADD_SEARCH_FIELD_QUERY_STRING } from 'types/actions/logs';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import FieldKey from './FieldKey';
|
||||
|
||||
interface SuggestedItemProps {
|
||||
name: string;
|
||||
type: string;
|
||||
applySuggestion: (name: string) => void;
|
||||
}
|
||||
function SuggestedItem({
|
||||
name,
|
||||
type,
|
||||
applySuggestion,
|
||||
}: SuggestedItemProps): JSX.Element {
|
||||
const addSuggestedField = (): void => {
|
||||
applySuggestion(name);
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
type="text"
|
||||
style={{ display: 'block', padding: '0.2rem' }}
|
||||
onClick={addSuggestedField}
|
||||
>
|
||||
<FieldKey name={name} type={type} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
interface SuggestionsProps {
|
||||
applySuggestion: (name: string) => void;
|
||||
}
|
||||
|
||||
function Suggestions({ applySuggestion }: SuggestionsProps): JSX.Element {
|
||||
const {
|
||||
fields: { selected },
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CategoryHeading>SUGGESTIONS</CategoryHeading>
|
||||
<div>
|
||||
{map(selected, (field) => (
|
||||
<SuggestedItem
|
||||
key={JSON.stringify(field)}
|
||||
name={field.name}
|
||||
type={field.type}
|
||||
applySuggestion={applySuggestion}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Suggestions;
|
||||
@@ -1,123 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { reverseParser } from 'lib/logql';
|
||||
import { flatten } from 'lodash-es';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { SearchFieldsActionBar } from './ActionBar';
|
||||
import QueryBuilder from './QueryBuilder/QueryBuilder';
|
||||
import Suggestions from './Suggestions';
|
||||
import {
|
||||
createParsedQueryStructure,
|
||||
fieldsQueryIsvalid,
|
||||
hashCode,
|
||||
initQueryKOVPair,
|
||||
prepareConditionOperator,
|
||||
QueryFields,
|
||||
} from './utils';
|
||||
|
||||
export interface SearchFieldsProps {
|
||||
onDropDownToggleHandler: (value: boolean) => VoidFunction;
|
||||
updateQueryString: (value: string) => void;
|
||||
}
|
||||
|
||||
function SearchFields({
|
||||
onDropDownToggleHandler,
|
||||
updateQueryString,
|
||||
}: SearchFieldsProps): JSX.Element {
|
||||
const {
|
||||
searchFilter: { parsedQuery },
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
|
||||
const [fieldsQuery, setFieldsQuery] = useState(
|
||||
createParsedQueryStructure([...parsedQuery] as never[]),
|
||||
);
|
||||
|
||||
const keyPrefixRef = useRef(hashCode(JSON.stringify(fieldsQuery)));
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
useEffect(() => {
|
||||
const updatedFieldsQuery = createParsedQueryStructure([
|
||||
...parsedQuery,
|
||||
] as never[]);
|
||||
setFieldsQuery(updatedFieldsQuery);
|
||||
const incomingHashCode = hashCode(JSON.stringify(updatedFieldsQuery));
|
||||
if (incomingHashCode !== keyPrefixRef.current) {
|
||||
keyPrefixRef.current = incomingHashCode;
|
||||
}
|
||||
}, [parsedQuery]);
|
||||
|
||||
// syncKeyPrefix initiates re-render. useful in situations like
|
||||
// delete field (in search panel). this method allows condiitonally
|
||||
// setting keyPrefix as doing it on every update of query initiates
|
||||
// a re-render. this is a problem for text fields where input focus goes away.
|
||||
const syncKeyPrefix = (): void => {
|
||||
keyPrefixRef.current = hashCode(JSON.stringify(fieldsQuery));
|
||||
};
|
||||
|
||||
const addSuggestedField = useCallback(
|
||||
(name: string): void => {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = [...fieldsQuery];
|
||||
|
||||
if (fieldsQuery.length > 0) {
|
||||
query.push([prepareConditionOperator()]);
|
||||
}
|
||||
|
||||
const newField: QueryFields[] = [];
|
||||
initQueryKOVPair(name).forEach((q) => newField.push(q));
|
||||
|
||||
query.push(newField);
|
||||
keyPrefixRef.current = hashCode(JSON.stringify(query));
|
||||
setFieldsQuery(query);
|
||||
},
|
||||
[fieldsQuery, setFieldsQuery],
|
||||
);
|
||||
|
||||
const applyUpdate = useCallback((): void => {
|
||||
const flatParsedQuery = flatten(fieldsQuery);
|
||||
|
||||
if (!fieldsQueryIsvalid(flatParsedQuery)) {
|
||||
notifications.error({
|
||||
message: 'Please enter a valid criteria for each of the selected fields',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
keyPrefixRef.current = hashCode(JSON.stringify(flatParsedQuery));
|
||||
updateQueryString(reverseParser(flatParsedQuery));
|
||||
onDropDownToggleHandler(false)();
|
||||
}, [fieldsQuery, notifications, onDropDownToggleHandler, updateQueryString]);
|
||||
|
||||
const clearFilters = useCallback((): void => {
|
||||
keyPrefixRef.current = hashCode(JSON.stringify([]));
|
||||
setFieldsQuery([]);
|
||||
updateQueryString('');
|
||||
}, [updateQueryString]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<QueryBuilder
|
||||
key={keyPrefixRef.current}
|
||||
keyPrefix={keyPrefixRef.current}
|
||||
onDropDownToggleHandler={onDropDownToggleHandler}
|
||||
fieldsQuery={fieldsQuery}
|
||||
setFieldsQuery={setFieldsQuery}
|
||||
syncKeyPrefix={syncKeyPrefix}
|
||||
/>
|
||||
<SearchFieldsActionBar
|
||||
applyUpdate={applyUpdate}
|
||||
clearFilters={clearFilters}
|
||||
/>
|
||||
<Suggestions applySuggestion={addSuggestedField} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default SearchFields;
|
||||
@@ -1,16 +0,0 @@
|
||||
import { blue } from '@ant-design/colors';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const QueryFieldContainer = styled.div`
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.1rem 0.5rem 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-radius: 0.25rem;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
&:hover {
|
||||
background: ${blue[6]};
|
||||
}
|
||||
`;
|
||||
@@ -1,137 +0,0 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
ConditionalOperators,
|
||||
QueryTypes,
|
||||
ValidTypeSequence,
|
||||
ValidTypeValue,
|
||||
} from 'lib/logql/tokens';
|
||||
|
||||
export interface QueryFields {
|
||||
type: keyof typeof QueryTypes;
|
||||
value: string | string[];
|
||||
}
|
||||
|
||||
export function fieldsQueryIsvalid(queryFields: QueryFields[]): boolean {
|
||||
let lastOp: string;
|
||||
let result = true;
|
||||
queryFields.forEach((q, idx) => {
|
||||
if (!q.value || q.value === null || q.value === '') {
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (Array.isArray(q.value) && q.value.length === 0) {
|
||||
result = false;
|
||||
}
|
||||
|
||||
const nextOp = idx < queryFields.length ? queryFields[idx + 1] : undefined;
|
||||
if (!ValidTypeSequence(lastOp?.type, q?.type, nextOp?.type)) {
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (!ValidTypeValue(lastOp?.value, q.value)) {
|
||||
result = false;
|
||||
}
|
||||
lastOp = q;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export const queryKOVPair = (): QueryFields[] => [
|
||||
{
|
||||
type: QueryTypes.QUERY_KEY,
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
type: QueryTypes.QUERY_OPERATOR,
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
type: QueryTypes.QUERY_VALUE,
|
||||
value: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const initQueryKOVPair = (
|
||||
name: string = null,
|
||||
op: string = null,
|
||||
value: string | string[] = null,
|
||||
): QueryFields[] => [
|
||||
{
|
||||
type: QueryTypes.QUERY_KEY,
|
||||
value: name,
|
||||
},
|
||||
{
|
||||
type: QueryTypes.QUERY_OPERATOR,
|
||||
value: op,
|
||||
},
|
||||
{
|
||||
type: QueryTypes.QUERY_VALUE,
|
||||
value: value,
|
||||
},
|
||||
];
|
||||
|
||||
export const prepareConditionOperator = (
|
||||
op: string = ConditionalOperators.AND,
|
||||
): QueryFields => {
|
||||
return {
|
||||
type: QueryTypes.CONDITIONAL_OPERATOR,
|
||||
value: op,
|
||||
};
|
||||
};
|
||||
|
||||
export const createParsedQueryStructure = (
|
||||
parsedQuery = [],
|
||||
): QueryFields[][] => {
|
||||
if (parsedQuery.length === 0) {
|
||||
return parsedQuery;
|
||||
}
|
||||
|
||||
const structuredArray = [queryKOVPair()];
|
||||
|
||||
let cond;
|
||||
let qCtr = -1;
|
||||
parsedQuery.forEach((query) => {
|
||||
if (cond) {
|
||||
structuredArray.push(cond);
|
||||
structuredArray.push(queryKOVPair());
|
||||
cond = null;
|
||||
qCtr = -1;
|
||||
}
|
||||
const stagingArr = structuredArray.at(-1);
|
||||
const prevQuery =
|
||||
Array.isArray(stagingArr) && qCtr >= 0 ? stagingArr[qCtr] : null;
|
||||
|
||||
if (query.type === QueryTypes.QUERY_KEY) {
|
||||
stagingArr[qCtr + 1] = query;
|
||||
} else if (
|
||||
query.type === QueryTypes.QUERY_OPERATOR &&
|
||||
prevQuery &&
|
||||
prevQuery.type === QueryTypes.QUERY_KEY
|
||||
) {
|
||||
stagingArr[qCtr + 1] = query;
|
||||
} else if (
|
||||
query.type === QueryTypes.QUERY_VALUE &&
|
||||
prevQuery &&
|
||||
prevQuery.type === QueryTypes.QUERY_OPERATOR
|
||||
) {
|
||||
stagingArr[qCtr + 1] = query;
|
||||
} else if (query.type === QueryTypes.CONDITIONAL_OPERATOR) {
|
||||
cond = query;
|
||||
}
|
||||
qCtr++;
|
||||
});
|
||||
return structuredArray;
|
||||
};
|
||||
|
||||
export const hashCode = (s: string): string => {
|
||||
if (!s) {
|
||||
return '0';
|
||||
}
|
||||
return `${Math.abs(
|
||||
[...s].reduce((a, b) => {
|
||||
a = (a << 5) - a + b.codePointAt(0);
|
||||
return a & a;
|
||||
}, 0),
|
||||
)}`;
|
||||
};
|
||||
@@ -1,230 +0,0 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { Input, InputRef, Popover } from 'antd';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import getStep from 'lib/getStep';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { getIdConditions } from 'pages/Logs/utils';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { GetLogsFields } from 'store/actions/logs/getFields';
|
||||
import { getLogs } from 'store/actions/logs/getLogs';
|
||||
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
FLUSH_LOGS,
|
||||
SET_LOADING,
|
||||
SET_LOADING_AGGREGATE,
|
||||
TOGGLE_LIVE_TAIL,
|
||||
} from 'types/actions/logs';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import SearchFields from './SearchFields';
|
||||
import { Container, DropDownContainer } from './styles';
|
||||
import { useSearchParser } from './useSearchParser';
|
||||
|
||||
function SearchFilter({
|
||||
getLogs,
|
||||
getLogsAggregate,
|
||||
getLogsFields,
|
||||
}: SearchFilterProps): JSX.Element {
|
||||
const { updateQueryString, queryString } = useSearchParser();
|
||||
const [searchText, setSearchText] = useState(queryString);
|
||||
const [showDropDown, setShowDropDown] = useState(false);
|
||||
const searchRef = useRef<InputRef>(null);
|
||||
const { logLinesPerPage, idEnd, idStart, liveTail, order } = useSelector<
|
||||
AppState,
|
||||
ILogsReducer
|
||||
>((state) => state.logs);
|
||||
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
// keep sync with url queryString
|
||||
useEffect(() => {
|
||||
setSearchText(queryString);
|
||||
}, [queryString]);
|
||||
|
||||
const debouncedupdateQueryString = useMemo(
|
||||
() => debounce(updateQueryString, 300),
|
||||
[updateQueryString],
|
||||
);
|
||||
|
||||
const onDropDownToggleHandler = useCallback(
|
||||
(value: boolean) => (): void => {
|
||||
setShowDropDown(value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(customQuery: string) => {
|
||||
getLogsFields();
|
||||
const { maxTime, minTime } = globalTime;
|
||||
|
||||
if (liveTail === 'PLAYING') {
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: 'PAUSED',
|
||||
});
|
||||
dispatch({
|
||||
type: FLUSH_LOGS,
|
||||
});
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: liveTail,
|
||||
});
|
||||
dispatch({
|
||||
type: SET_LOADING,
|
||||
payload: false,
|
||||
});
|
||||
|
||||
getLogsAggregate({
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
step: getStep({
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
inputFormat: 'ns',
|
||||
}),
|
||||
q: customQuery,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
});
|
||||
} else {
|
||||
getLogs({
|
||||
q: customQuery,
|
||||
limit: logLinesPerPage,
|
||||
orderBy: 'timestamp',
|
||||
order,
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
...getIdConditions(idStart, idEnd, order),
|
||||
});
|
||||
|
||||
getLogsAggregate({
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
step: getStep({
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
inputFormat: 'ns',
|
||||
}),
|
||||
q: customQuery,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
dispatch,
|
||||
getLogs,
|
||||
getLogsAggregate,
|
||||
idEnd,
|
||||
idStart,
|
||||
liveTail,
|
||||
logLinesPerPage,
|
||||
globalTime,
|
||||
getLogsFields,
|
||||
order,
|
||||
],
|
||||
);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
const urlQueryString = urlQuery.get('q');
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({
|
||||
type: SET_LOADING,
|
||||
payload: true,
|
||||
});
|
||||
dispatch({
|
||||
type: SET_LOADING_AGGREGATE,
|
||||
payload: true,
|
||||
});
|
||||
|
||||
const debouncedHandleSearch = debounce(handleSearch, 600);
|
||||
|
||||
debouncedHandleSearch(urlQueryString || '');
|
||||
|
||||
return (): void => {
|
||||
debouncedHandleSearch.cancel();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
urlQueryString,
|
||||
idEnd,
|
||||
idStart,
|
||||
logLinesPerPage,
|
||||
dispatch,
|
||||
globalTime.maxTime,
|
||||
globalTime.minTime,
|
||||
order,
|
||||
]);
|
||||
|
||||
const onPopOverChange = useCallback(
|
||||
(isVisible: boolean) => {
|
||||
onDropDownToggleHandler(isVisible)();
|
||||
},
|
||||
[onDropDownToggleHandler],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Popover
|
||||
getPopupContainer={popupContainer}
|
||||
placement="bottom"
|
||||
content={
|
||||
<DropDownContainer>
|
||||
<SearchFields
|
||||
updateQueryString={updateQueryString}
|
||||
onDropDownToggleHandler={onDropDownToggleHandler}
|
||||
/>
|
||||
</DropDownContainer>
|
||||
}
|
||||
trigger="click"
|
||||
overlayInnerStyle={{
|
||||
width: `${searchRef?.current?.input?.offsetWidth || 0}px`,
|
||||
}}
|
||||
open={showDropDown}
|
||||
destroyTooltipOnHide
|
||||
onOpenChange={onPopOverChange}
|
||||
>
|
||||
<Input.Search
|
||||
ref={searchRef}
|
||||
placeholder="Search Filter"
|
||||
value={searchText}
|
||||
onChange={(e): void => {
|
||||
const { value } = e.target;
|
||||
setSearchText(value);
|
||||
}}
|
||||
onSearch={debouncedupdateQueryString}
|
||||
allowClear
|
||||
/>
|
||||
</Popover>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
getLogs: typeof getLogs;
|
||||
getLogsAggregate: typeof getLogsAggregate;
|
||||
getLogsFields: typeof GetLogsFields;
|
||||
}
|
||||
|
||||
type SearchFilterProps = DispatchProps;
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getLogs: bindActionCreators(getLogs, dispatch),
|
||||
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||
getLogsFields: bindActionCreators(GetLogsFields, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(memo(SearchFilter));
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Card } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const DropDownContainer = styled(Card)`
|
||||
.ant-card-body {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Container = styled.div`
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
`;
|
||||
@@ -1,81 +0,0 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||
import history from 'lib/history';
|
||||
import { parseQuery } from 'lib/logql';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
SET_SEARCH_QUERY_PARSED_PAYLOAD,
|
||||
SET_SEARCH_QUERY_STRING,
|
||||
} from 'types/actions/logs';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { getGlobalTime } from './utils';
|
||||
|
||||
export function useSearchParser(): {
|
||||
queryString: string;
|
||||
parsedQuery: unknown;
|
||||
updateQueryString: (arg0: string) => void;
|
||||
} {
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const {
|
||||
searchFilter: { parsedQuery, queryString },
|
||||
order,
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
const parsedFilters = urlQuery.get('q');
|
||||
|
||||
const { minTime, maxTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((store) => store.globalTime);
|
||||
|
||||
const updateQueryString = useCallback(
|
||||
(updatedQueryString: string) => {
|
||||
history.replace({
|
||||
pathname: history.location.pathname,
|
||||
search: `?${QueryParams.q}=${updatedQueryString}&${QueryParams.order}=${order}`,
|
||||
});
|
||||
|
||||
const globalTime = getMinMaxForSelectedTime(selectedTime, minTime, maxTime);
|
||||
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_STRING,
|
||||
payload: {
|
||||
searchQueryString: updatedQueryString,
|
||||
globalTime: getGlobalTime(selectedTime, globalTime),
|
||||
},
|
||||
});
|
||||
|
||||
const parsedQueryFromString = parseQuery(updatedQueryString);
|
||||
if (!isEqual(parsedQuery, parsedQueryFromString)) {
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_PARSED_PAYLOAD,
|
||||
payload: parsedQueryFromString,
|
||||
});
|
||||
}
|
||||
},
|
||||
// need to hide this warning as we don't want to update the query string on every change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[dispatch, parsedQuery, selectedTime, queryString],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateQueryString(parsedFilters || '');
|
||||
}, [parsedFilters, updateQueryString]);
|
||||
|
||||
return {
|
||||
queryString,
|
||||
parsedQuery,
|
||||
updateQueryString,
|
||||
};
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { GetMinMaxPayload } from 'lib/getMinMax';
|
||||
|
||||
export const getGlobalTime = (
|
||||
selectedTime: Time | CustomTimeType,
|
||||
globalTime: GetMinMaxPayload,
|
||||
): GetMinMaxPayload | undefined => {
|
||||
if (selectedTime === 'custom') {
|
||||
return undefined;
|
||||
}
|
||||
return globalTime;
|
||||
};
|
||||
@@ -1,164 +0,0 @@
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { Card } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
// components
|
||||
import ListLogView from 'components/Logs/ListLogView';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import LogsTableView from 'components/Logs/TableView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { CARD_BODY_STYLE } from 'constants/card';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
// interfaces
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
// styles
|
||||
import { Container, Heading } from './styles';
|
||||
|
||||
import './logsTable.styles.scss';
|
||||
|
||||
export type LogViewMode = 'raw' | 'table' | 'list';
|
||||
|
||||
type LogsTableProps = {
|
||||
viewMode: LogViewMode;
|
||||
linesPerRow: number;
|
||||
};
|
||||
|
||||
function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
const { viewMode, linesPerRow } = props;
|
||||
|
||||
const { activeLog, onClearActiveLog, onAddToQuery, onSetActiveLog } =
|
||||
useActiveLog();
|
||||
|
||||
const {
|
||||
logs,
|
||||
fields: { selected },
|
||||
isLoading,
|
||||
liveTail,
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
|
||||
const isLiveTail = useMemo(
|
||||
() => logs.length === 0 && liveTail === 'PLAYING',
|
||||
[logs?.length, liveTail],
|
||||
);
|
||||
|
||||
const isNoLogs = useMemo(
|
||||
() => logs.length === 0 && liveTail === 'STOPPED',
|
||||
[logs?.length, liveTail],
|
||||
);
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
// this component will alwyays be called on old logs explorer page itself!
|
||||
dataSource: DataSource.LOGS,
|
||||
// and we do not have table / timeseries aggregated views in the old logs explorer!
|
||||
aggregateOperator: StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(index: number): JSX.Element => {
|
||||
const log = logs[index];
|
||||
|
||||
if (viewMode === 'raw') {
|
||||
return (
|
||||
<RawLogView
|
||||
key={log.id}
|
||||
data={log}
|
||||
linesPerRow={linesPerRow}
|
||||
selectedFields={selected}
|
||||
fontSize={options.fontSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListLogView
|
||||
key={log.id}
|
||||
logData={log}
|
||||
selectedFields={selected}
|
||||
linesPerRow={linesPerRow}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onSetActiveLog={onSetActiveLog}
|
||||
fontSize={options.fontSize}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[
|
||||
logs,
|
||||
viewMode,
|
||||
selected,
|
||||
linesPerRow,
|
||||
onAddToQuery,
|
||||
onSetActiveLog,
|
||||
options.fontSize,
|
||||
],
|
||||
);
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
if (viewMode === 'table') {
|
||||
return (
|
||||
<LogsTableView
|
||||
onClickExpand={onSetActiveLog}
|
||||
logs={logs}
|
||||
fields={selected}
|
||||
linesPerRow={linesPerRow}
|
||||
fontSize={options.fontSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="logs-card" bodyStyle={CARD_BODY_STYLE}>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso totalCount={logs.length} itemContent={getItemContent} />
|
||||
</OverlayScrollbar>
|
||||
</Card>
|
||||
);
|
||||
}, [
|
||||
getItemContent,
|
||||
linesPerRow,
|
||||
logs,
|
||||
onSetActiveLog,
|
||||
options.fontSize,
|
||||
selected,
|
||||
viewMode,
|
||||
]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner height={20} tip="Getting Logs" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{viewMode !== 'table' && (
|
||||
<Heading>
|
||||
<Typography.Text>Event</Typography.Text>
|
||||
</Heading>
|
||||
)}
|
||||
|
||||
{isLiveTail && <Typography>Getting live logs...</Typography>}
|
||||
|
||||
{isNoLogs && <Typography>No logs lines found</Typography>}
|
||||
|
||||
{renderContent}
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LogsTable);
|
||||
@@ -1,3 +0,0 @@
|
||||
.logs-card {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Card } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export const Heading = styled(Card)`
|
||||
margin-bottom: 0.1rem;
|
||||
height: 32px;
|
||||
.ant-card-body {
|
||||
padding: 0.3rem 0.5rem;
|
||||
}
|
||||
`;
|
||||
@@ -1,8 +1,6 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
export const buttonText: Record<string, string> = {
|
||||
[ROUTES.LOGS_EXPLORER]: 'Old Explorer',
|
||||
[ROUTES.TRACE]: 'New Explorer',
|
||||
[ROUTES.OLD_LOGS_EXPLORER]: 'New Explorer',
|
||||
[ROUTES.TRACES_EXPLORER]: 'Old Explorer',
|
||||
};
|
||||
|
||||
@@ -15,11 +15,9 @@ function NewExplorerCTA(): JSX.Element | null {
|
||||
const location = useLocation();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const isTraceOrLogsExplorerPage = useMemo(
|
||||
const isTracesExplorerPage = useMemo(
|
||||
() =>
|
||||
location.pathname === ROUTES.LOGS_EXPLORER ||
|
||||
location.pathname === ROUTES.TRACE ||
|
||||
location.pathname === ROUTES.OLD_LOGS_EXPLORER ||
|
||||
location.pathname === ROUTES.TRACES_EXPLORER,
|
||||
[location.pathname],
|
||||
);
|
||||
@@ -27,12 +25,8 @@ function NewExplorerCTA(): JSX.Element | null {
|
||||
const onClickHandler = useCallback(
|
||||
(e?: React.MouseEvent): void => {
|
||||
let targetPath: string;
|
||||
if (location.pathname === ROUTES.LOGS_EXPLORER) {
|
||||
targetPath = ROUTES.OLD_LOGS_EXPLORER;
|
||||
} else if (location.pathname === ROUTES.TRACE) {
|
||||
if (location.pathname === ROUTES.TRACE) {
|
||||
targetPath = ROUTES.TRACES_EXPLORER;
|
||||
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
|
||||
targetPath = ROUTES.LOGS_EXPLORER;
|
||||
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
|
||||
targetPath = ROUTES.TRACE;
|
||||
} else {
|
||||
@@ -58,7 +52,7 @@ function NewExplorerCTA(): JSX.Element | null {
|
||||
[location.pathname, onClickHandler],
|
||||
);
|
||||
|
||||
if (!isTraceOrLogsExplorerPage) {
|
||||
if (!isTracesExplorerPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -66,10 +60,6 @@ function NewExplorerCTA(): JSX.Element | null {
|
||||
return button;
|
||||
}
|
||||
|
||||
if (location.pathname === ROUTES.LOGS_EXPLORER) {
|
||||
return button;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="new-explorer-cta-with-badge">
|
||||
{button}
|
||||
|
||||
@@ -3,10 +3,9 @@ import './FormatField.styles.scss';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
|
||||
import { FieldTitle } from '../styles';
|
||||
import { OptionsMenuConfig } from '../types';
|
||||
import { LogViewMode, OptionsMenuConfig } from '../types';
|
||||
import { FormatFieldWrapper } from './styles';
|
||||
|
||||
function FormatField({ config }: FormatFieldProps): JSX.Element | null {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { InputNumberProps, RadioProps, SelectProps } from 'antd';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
|
||||
export type LogViewMode = 'raw' | 'table' | 'list';
|
||||
|
||||
export enum FontSize {
|
||||
SMALL = 'small',
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useQueries } from 'react-query';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
import {
|
||||
FontSize,
|
||||
InitialOptions,
|
||||
LogViewMode,
|
||||
OptionsMenuConfig,
|
||||
OptionsQuery,
|
||||
} from './types';
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { OPERATORS, QueryBuilderKeys } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
|
||||
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { UseActiveLog } from './types';
|
||||
@@ -40,22 +32,10 @@ export function getOldLogsOperatorFromNew(operator: string): string {
|
||||
}
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export const useActiveLog = (): UseActiveLog => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
searchFilter: { queryString },
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
const queryClient = useQueryClient();
|
||||
const { pathname } = useLocation();
|
||||
const history = useHistory();
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const isLogsPage = useMemo(
|
||||
() => pathname === ROUTES.OLD_LOGS_EXPLORER,
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const [activeLog, setActiveLog] = useState<ILog | null>(null);
|
||||
|
||||
// Close drawer/clear active log when query in URL changes
|
||||
@@ -72,26 +52,9 @@ export const useActiveLog = (): UseActiveLog => {
|
||||
prevQueryRef.current = compositeQuery;
|
||||
}, [compositeQuery]);
|
||||
|
||||
const onSetDetailedLogData = useCallback(
|
||||
(logData: ILog) => {
|
||||
dispatch({
|
||||
type: SET_DETAILED_LOG_DATA,
|
||||
payload: logData,
|
||||
});
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onSetActiveLog = useCallback(
|
||||
(nextActiveLog: ILog): void => {
|
||||
if (isLogsPage) {
|
||||
onSetDetailedLogData(nextActiveLog);
|
||||
} else {
|
||||
setActiveLog(nextActiveLog);
|
||||
}
|
||||
},
|
||||
[isLogsPage, onSetDetailedLogData],
|
||||
);
|
||||
const onSetActiveLog = useCallback((nextActiveLog: ILog): void => {
|
||||
setActiveLog(nextActiveLog);
|
||||
}, []);
|
||||
|
||||
const onClearActiveLog = useCallback((): void => setActiveLog(null), []);
|
||||
|
||||
@@ -204,26 +167,12 @@ export const useActiveLog = (): UseActiveLog => {
|
||||
},
|
||||
[currentQuery, notifications, queryClient, redirectWithQueryBuilderData],
|
||||
);
|
||||
const onAddToQueryLogs = useCallback(
|
||||
(fieldKey: string, fieldValue: string, operator: string) => {
|
||||
const newOperator = getOldLogsOperatorFromNew(operator);
|
||||
const updatedQueryString = getGeneratedFilterQueryString(
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
newOperator,
|
||||
queryString,
|
||||
);
|
||||
|
||||
history.replace(`${ROUTES.OLD_LOGS_EXPLORER}?q=${updatedQueryString}`);
|
||||
},
|
||||
[history, queryString],
|
||||
);
|
||||
|
||||
return {
|
||||
activeLog,
|
||||
onSetActiveLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery: isLogsPage ? onAddToQueryLogs : onAddToQueryExplorer,
|
||||
onAddToQuery: onAddToQueryExplorer,
|
||||
onGroupByAttribute,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
QueryKey,
|
||||
useQuery,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
import { ErrorType } from 'api/generatedAPIInstance';
|
||||
import {
|
||||
RenderErrorResponseDTO,
|
||||
TelemetrytypesTelemetryFieldValuesDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { getFieldValueSuggestions } from 'api/querySuggestions/getFieldValueSuggestions';
|
||||
import { FIELD_API_CACHE_TIME } from 'constants/queryCacheTime';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
FieldValuesConfig,
|
||||
FieldValuesResponse,
|
||||
} from 'api/querySuggestions/types';
|
||||
import { BuilderQueryType } from 'types/api/v5/queryRange';
|
||||
|
||||
export type FieldValuesQueryOptions = UseQueryOptions<
|
||||
FieldValuesResponse,
|
||||
ErrorType<RenderErrorResponseDTO>,
|
||||
TelemetrytypesTelemetryFieldValuesDTO
|
||||
> & { queryKey: QueryKey };
|
||||
|
||||
const EMPTY_FIELD_VALUES: TelemetrytypesTelemetryFieldValuesDTO = {};
|
||||
|
||||
export const toFieldValues = (
|
||||
res: FieldValuesResponse | undefined,
|
||||
): TelemetrytypesTelemetryFieldValuesDTO =>
|
||||
res?.data?.values ?? EMPTY_FIELD_VALUES;
|
||||
|
||||
export const getFieldValuesQueryOptions = (
|
||||
fieldValuesConfig: FieldValuesConfig,
|
||||
builderQueryType?: BuilderQueryType,
|
||||
): FieldValuesQueryOptions => ({
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.FIELD_VALUES_SUGGESTION,
|
||||
builderQueryType,
|
||||
fieldValuesConfig,
|
||||
],
|
||||
queryFn: ({ signal }): Promise<FieldValuesResponse> =>
|
||||
getFieldValueSuggestions(fieldValuesConfig, builderQueryType, signal),
|
||||
select: toFieldValues,
|
||||
cacheTime: FIELD_API_CACHE_TIME,
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
export const useFieldValuesSuggestion = (
|
||||
fieldValuesConfig: FieldValuesConfig,
|
||||
builderQueryType?: BuilderQueryType,
|
||||
options?: Pick<FieldValuesQueryOptions, 'enabled'>,
|
||||
): UseQueryResult<
|
||||
TelemetrytypesTelemetryFieldValuesDTO,
|
||||
ErrorType<RenderErrorResponseDTO>
|
||||
> =>
|
||||
useQuery({
|
||||
...getFieldValuesQueryOptions(fieldValuesConfig, builderQueryType),
|
||||
...options,
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { logqlQueries } from 'lib/__fixtures__/logql';
|
||||
import parser from 'lib/logql/parser';
|
||||
|
||||
describe('lib/logql/parser', () => {
|
||||
it('parse valid queries', () => {
|
||||
logqlQueries.forEach((queryObject) => {
|
||||
expect(parser(queryObject.query)).toStrictEqual(queryObject.parsedQuery);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { logqlQueries } from 'lib/__fixtures__/logql';
|
||||
import { reverseParser } from 'lib/logql/reverseParser';
|
||||
|
||||
describe('lib/logql/reverseParser', () => {
|
||||
it('reverse parse valid queries', () => {
|
||||
logqlQueries.forEach((queryObject) => {
|
||||
expect(reverseParser(queryObject.parsedQuery)).toStrictEqual(
|
||||
queryObject.query,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { logqlQueries } from 'lib/__fixtures__/logql';
|
||||
import { splitter } from 'lib/logql/splitter';
|
||||
|
||||
describe('lib/logql/splitter', () => {
|
||||
it('splitter valid quereies', () => {
|
||||
logqlQueries.forEach((queryObject) => {
|
||||
expect(splitter(queryObject.query)).toStrictEqual(queryObject.splitterQuery);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import { generateFilterQuery } from './logs/generateFilterQuery';
|
||||
|
||||
export const getGeneratedFilterQueryString = (
|
||||
fieldKey: string,
|
||||
fieldValue: string,
|
||||
operator: string,
|
||||
queryString: string,
|
||||
): string => {
|
||||
let updatedQueryString = queryString || '';
|
||||
|
||||
const generatedString = generateFilterQuery({
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
type: operator,
|
||||
});
|
||||
|
||||
if (updatedQueryString.length === 0) {
|
||||
updatedQueryString += `${generatedString}`;
|
||||
} else {
|
||||
updatedQueryString += ` AND ${generatedString}`;
|
||||
}
|
||||
|
||||
return updatedQueryString;
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
export class ErrorConvertToFullText extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ErrorConvertToFullText';
|
||||
Object.setPrototypeOf(this, ErrorConvertToFullText.prototype);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export class ErrorInvalidQueryPair extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ErrorInvalidQueryPair';
|
||||
Object.setPrototypeOf(this, ErrorInvalidQueryPair.prototype);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './ConvertToFullText';
|
||||
export * from './InvalidQueryPair';
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './parser';
|
||||
export * from './reverseParser';
|
||||
export * from './splitter';
|
||||
export * from './tokens';
|
||||
@@ -1,149 +0,0 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
ErrorConvertToFullText,
|
||||
ErrorInvalidQueryPair,
|
||||
} from 'lib/logql/errors';
|
||||
import splitter from 'lib/logql/splitter';
|
||||
import {
|
||||
ConditionalOperators,
|
||||
QueryOperatorsMultiVal,
|
||||
QueryOperatorsSingleVal,
|
||||
QueryTypes,
|
||||
} from 'lib/logql/tokens';
|
||||
|
||||
const validateMultiValue = (queryToken: string): boolean => {
|
||||
const queryValues = [];
|
||||
let start;
|
||||
let isQuoteStart = false;
|
||||
if (queryToken[0] === '(' && queryToken[queryToken.length - 1] === ')') {
|
||||
for (let idx = 1; idx < queryToken.length - 1; idx += 1) {
|
||||
if (queryToken[idx] === "'") {
|
||||
if (queryToken[idx - 1] === '\\') {
|
||||
// skip
|
||||
} else if (isQuoteStart) {
|
||||
isQuoteStart = false;
|
||||
queryValues.push(queryToken.slice(start, idx));
|
||||
} else {
|
||||
isQuoteStart = true;
|
||||
start = idx + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return queryValues;
|
||||
};
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export const parseQuery = (queryString): any => {
|
||||
let parsedRaw = [];
|
||||
const generateQuery = (queryToken): void => {
|
||||
const prevToken = parsedRaw[parsedRaw.length - 1];
|
||||
|
||||
// Is a QUERY_KEY
|
||||
if (
|
||||
prevToken === undefined ||
|
||||
prevToken.type === QueryTypes.CONDITIONAL_OPERATOR
|
||||
) {
|
||||
parsedRaw.push({
|
||||
type: QueryTypes.QUERY_KEY,
|
||||
value: queryToken,
|
||||
});
|
||||
}
|
||||
// Is a QUERY_OPERATOR
|
||||
else if (prevToken && prevToken.type === QueryTypes.QUERY_KEY) {
|
||||
if (
|
||||
Object.values({
|
||||
...QueryOperatorsMultiVal,
|
||||
...QueryOperatorsSingleVal,
|
||||
}).find((op) => op.toLowerCase() === queryToken.toLowerCase())
|
||||
) {
|
||||
parsedRaw.push({
|
||||
type: QueryTypes.QUERY_OPERATOR,
|
||||
value: queryToken,
|
||||
});
|
||||
} else {
|
||||
throw new ErrorInvalidQueryPair(
|
||||
'Expected conditional operator received',
|
||||
queryToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Is a QUERY_VALUE
|
||||
else if (prevToken && prevToken.type === QueryTypes.QUERY_OPERATOR) {
|
||||
// Check for multi value
|
||||
let value = queryToken;
|
||||
// if (
|
||||
// typeof queryToken === 'string' &&
|
||||
// queryToken.length >= 2 &&
|
||||
// queryToken[0] === "'" &&
|
||||
// queryToken[queryToken.length - 1] === "'"
|
||||
// ) {
|
||||
// value = queryToken.slice(1, queryToken.length - 1);
|
||||
// }
|
||||
if (
|
||||
Object.values(QueryOperatorsMultiVal).some(
|
||||
(operator) => operator.toLowerCase() === prevToken.value.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
value = validateMultiValue(queryToken);
|
||||
if (value === false) {
|
||||
throw new ErrorConvertToFullText();
|
||||
}
|
||||
}
|
||||
|
||||
parsedRaw.push({
|
||||
type: QueryTypes.QUERY_VALUE,
|
||||
value,
|
||||
});
|
||||
} else if (prevToken && prevToken.type === QueryTypes.QUERY_VALUE) {
|
||||
if (
|
||||
Object.values(ConditionalOperators).find(
|
||||
(op) => op.toLowerCase() === queryToken.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
parsedRaw.push({
|
||||
type: QueryTypes.CONDITIONAL_OPERATOR,
|
||||
value: queryToken,
|
||||
});
|
||||
} else {
|
||||
throw new ErrorInvalidQueryPair(
|
||||
'Expected conditional operator received',
|
||||
queryToken,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Not a Key
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const spaceSplittedQUery = splitter(queryString);
|
||||
spaceSplittedQUery.forEach((q) => {
|
||||
generateQuery(q);
|
||||
});
|
||||
} catch (e: Error) {
|
||||
if (e instanceof ErrorInvalidQueryPair) {
|
||||
//
|
||||
} else if (e instanceof ErrorConvertToFullText) {
|
||||
parsedRaw = [
|
||||
{
|
||||
type: QueryTypes.QUERY_KEY,
|
||||
value: 'FULLTEXT',
|
||||
},
|
||||
{
|
||||
type: QueryTypes.QUERY_OPERATOR,
|
||||
value: 'CONTAINS',
|
||||
},
|
||||
{
|
||||
type: QueryTypes.QUERY_VALUE,
|
||||
value: String.raw`${queryString}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
return parsedRaw;
|
||||
};
|
||||
|
||||
export default parseQuery;
|
||||
@@ -1,39 +0,0 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { QueryTypes, StringTypeQueryOperators } from './tokens';
|
||||
|
||||
export const reverseParser = (
|
||||
parserQueryArr: { type: string; value: any }[] = [],
|
||||
): string => {
|
||||
let queryString = '';
|
||||
let lastToken: { type: string; value: any };
|
||||
parserQueryArr.forEach((query) => {
|
||||
if (queryString) {
|
||||
queryString += ' ';
|
||||
}
|
||||
|
||||
if (Array.isArray(query.value) && query.value.length > 0) {
|
||||
// if the values are array type, here we spread them in
|
||||
// ('a', 'b') format
|
||||
const queryValuesMapped = query.value.map((val) => `'${val}'`).join(',');
|
||||
queryString += `(${queryValuesMapped})`;
|
||||
} else {
|
||||
if (
|
||||
query.type === QueryTypes.QUERY_VALUE &&
|
||||
lastToken.type === QueryTypes.QUERY_OPERATOR &&
|
||||
Object.values(StringTypeQueryOperators).includes(lastToken.value)
|
||||
) {
|
||||
// for operators that need string type value, here we append single
|
||||
// quotes. if the content has single quote they would be removed
|
||||
queryString += `'${query.value?.replace(/'/g, '')}'`;
|
||||
} else {
|
||||
queryString += query.value;
|
||||
}
|
||||
}
|
||||
lastToken = query;
|
||||
});
|
||||
|
||||
return queryString;
|
||||
};
|
||||
|
||||
export default reverseParser;
|
||||
@@ -1,51 +0,0 @@
|
||||
// @ts-nocheck
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export const splitter = (queryString: string): string[] => {
|
||||
const splittedParts: string[] = [];
|
||||
let start = 0;
|
||||
let isBracketStart = false;
|
||||
let isQuoteStart = false;
|
||||
|
||||
const pushPart = (idx): void => {
|
||||
splittedParts.push(queryString.slice(start, idx));
|
||||
start = idx + 1;
|
||||
};
|
||||
for (let idx = 0; idx < queryString.length; idx += 1) {
|
||||
const currentChar = queryString[idx];
|
||||
|
||||
if (currentChar === ' ') {
|
||||
if (!isBracketStart && !isQuoteStart) {
|
||||
pushPart(idx);
|
||||
}
|
||||
} else if (currentChar === '(') {
|
||||
isBracketStart = true;
|
||||
} else if (currentChar === ')') {
|
||||
if (queryString[idx - 1] !== '\\') {
|
||||
pushPart(idx + 1);
|
||||
isBracketStart = false;
|
||||
}
|
||||
if (isQuoteStart) {
|
||||
isQuoteStart = false;
|
||||
}
|
||||
} else if (currentChar === "'") {
|
||||
if (isQuoteStart) {
|
||||
if (queryString[idx - 1] !== '\\' && !isBracketStart) {
|
||||
pushPart(idx + 1);
|
||||
isQuoteStart = false;
|
||||
}
|
||||
} else {
|
||||
isQuoteStart = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process remaining part
|
||||
if (start < queryString.length) {
|
||||
pushPart(queryString.length);
|
||||
}
|
||||
|
||||
return splittedParts.map((s) => String.raw`${s}`).filter(Boolean);
|
||||
};
|
||||
|
||||
export default splitter;
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface ILogQLParsedQueryItem {
|
||||
type: string;
|
||||
value: string | string[];
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { InputNumber, Row, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
interface PopoverContentProps {
|
||||
linesPerRow: number;
|
||||
handleLinesPerRowChange: (l: unknown) => void;
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
linesPerRow,
|
||||
handleLinesPerRowChange,
|
||||
}: PopoverContentProps): JSX.Element {
|
||||
return (
|
||||
<Row align="middle">
|
||||
<Space align="center">
|
||||
<Typography>Max lines per Row </Typography>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={10}
|
||||
value={linesPerRow}
|
||||
onChange={handleLinesPerRowChange}
|
||||
/>
|
||||
</Space>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default PopoverContent;
|
||||
@@ -1,48 +0,0 @@
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
import { ViewModeOption } from './types';
|
||||
|
||||
export const viewModeOptionList: ViewModeOption[] = [
|
||||
{
|
||||
key: 'raw',
|
||||
label: 'Raw',
|
||||
value: 'raw',
|
||||
},
|
||||
{
|
||||
key: 'table',
|
||||
label: 'Table',
|
||||
value: 'table',
|
||||
},
|
||||
{
|
||||
key: 'list',
|
||||
label: 'List',
|
||||
value: 'list',
|
||||
},
|
||||
];
|
||||
|
||||
export const logsOptions = ['raw', 'table'];
|
||||
|
||||
export const defaultSelectStyle: CSSProperties = {
|
||||
minWidth: '6rem',
|
||||
};
|
||||
|
||||
export enum OrderPreferenceItems {
|
||||
DESC = 'desc',
|
||||
ASC = 'asc',
|
||||
}
|
||||
|
||||
export const orderItems: OrderPreference[] = [
|
||||
{
|
||||
name: 'Descending',
|
||||
enum: OrderPreferenceItems.DESC,
|
||||
},
|
||||
{
|
||||
name: 'Ascending',
|
||||
enum: OrderPreferenceItems.ASC,
|
||||
},
|
||||
];
|
||||
|
||||
export interface OrderPreference {
|
||||
name: string;
|
||||
enum: OrderPreferenceItems;
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
// utils
|
||||
import { useCallback, useLayoutEffect, useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import get from 'api/browser/localstorage/get';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
// interfaces
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { setLinesPerRow } from 'store/actions/logs/setLInesPerRow';
|
||||
// actions
|
||||
import { setViewMode } from 'store/actions/logs/setViewMode';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
||||
import { viewModeOptionList } from './config';
|
||||
import { SelectedLogViewData } from './types';
|
||||
import { isLogViewMode } from './utils';
|
||||
|
||||
export const useSelectedLogView = (): SelectedLogViewData => {
|
||||
const dispatch = useDispatch();
|
||||
const viewMode = useSelector<AppState, LogViewMode>(
|
||||
(state) => state.logs.viewMode,
|
||||
);
|
||||
const linesPerRow = useSelector<AppState, number>(
|
||||
(state) => state.logs.linesPerRow,
|
||||
);
|
||||
|
||||
const viewModeOption = useMemo(
|
||||
() =>
|
||||
viewModeOptionList.find(
|
||||
(viewModeOption) => viewModeOption.value === viewMode,
|
||||
) ?? viewModeOptionList[0],
|
||||
[viewMode],
|
||||
);
|
||||
|
||||
const handleViewModeChange = useCallback(
|
||||
(selectedViewMode: LogViewMode) => {
|
||||
dispatch(setViewMode(selectedViewMode));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleViewModeOptionChange = useCallback(
|
||||
({ key }: { key: string }) => {
|
||||
if (isLogViewMode(key)) {
|
||||
handleViewModeChange(key);
|
||||
}
|
||||
},
|
||||
[handleViewModeChange],
|
||||
);
|
||||
|
||||
const handleLinesPerRowChange = useCallback(
|
||||
(selectedLinesPerRow: unknown) => {
|
||||
if (typeof selectedLinesPerRow === 'number') {
|
||||
dispatch(setLinesPerRow(selectedLinesPerRow));
|
||||
}
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const storedViewMode = get(LOCALSTORAGE.LOGS_VIEW_MODE);
|
||||
if (storedViewMode) {
|
||||
handleViewModeChange(storedViewMode as LogViewMode);
|
||||
}
|
||||
|
||||
const storedLinesPerRow = get(LOCALSTORAGE.LOGS_LINES_PER_ROW);
|
||||
if (storedLinesPerRow) {
|
||||
handleLinesPerRowChange(+storedLinesPerRow);
|
||||
}
|
||||
}, [handleViewModeChange, handleLinesPerRowChange]);
|
||||
|
||||
return {
|
||||
viewModeOptionList,
|
||||
viewModeOption,
|
||||
viewMode,
|
||||
handleViewModeChange,
|
||||
handleViewModeOptionChange,
|
||||
linesPerRow,
|
||||
handleLinesPerRowChange,
|
||||
};
|
||||
};
|
||||
@@ -1,156 +0,0 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Button, Col, Popover, Row, Select, Space } from 'antd';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import LogControls from 'container/LogControls';
|
||||
import LogDetailedView from 'container/LogDetailedView';
|
||||
import LogLiveTail from 'container/LogLiveTail';
|
||||
import LogsAggregate from 'container/LogsAggregate';
|
||||
import LogsFilters from 'container/LogsFilters';
|
||||
import LogsSearchFilter from 'container/LogsSearchFilter';
|
||||
import LogsTable from 'container/LogsTable';
|
||||
import history from 'lib/history';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { SET_LOGS_ORDER } from 'types/actions/logs';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import {
|
||||
defaultSelectStyle,
|
||||
logsOptions,
|
||||
orderItems,
|
||||
OrderPreferenceItems,
|
||||
} from './config';
|
||||
import { useSelectedLogView } from './hooks';
|
||||
import PopoverContent from './PopoverContent';
|
||||
import SpaceContainer from './styles';
|
||||
|
||||
import './logs.styles.scss';
|
||||
|
||||
function OldLogsExplorer(): JSX.Element {
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const { order } = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
const location = useLocation();
|
||||
|
||||
const {
|
||||
viewModeOptionList,
|
||||
viewModeOption,
|
||||
viewMode,
|
||||
handleViewModeOptionChange,
|
||||
linesPerRow,
|
||||
handleLinesPerRowChange,
|
||||
} = useSelectedLogView();
|
||||
|
||||
const renderPopoverContent = useCallback(
|
||||
() => (
|
||||
<PopoverContent
|
||||
linesPerRow={linesPerRow}
|
||||
handleLinesPerRowChange={handleLinesPerRowChange}
|
||||
/>
|
||||
),
|
||||
[linesPerRow, handleLinesPerRowChange],
|
||||
);
|
||||
|
||||
const isFormatButtonVisible = useMemo(
|
||||
() => logsOptions.includes(viewMode),
|
||||
[viewMode],
|
||||
);
|
||||
|
||||
const selectedViewModeOption = useMemo(
|
||||
() => viewModeOption.value.toString(),
|
||||
[viewModeOption.value],
|
||||
);
|
||||
|
||||
const onChangeVeiwMode = useCallback(
|
||||
(key: string) => {
|
||||
handleViewModeOptionChange({
|
||||
key,
|
||||
});
|
||||
},
|
||||
[handleViewModeOptionChange],
|
||||
);
|
||||
|
||||
const handleChangeOrder = (value: OrderPreferenceItems): void => {
|
||||
dispatch({
|
||||
type: SET_LOGS_ORDER,
|
||||
payload: value,
|
||||
});
|
||||
const params = new URLSearchParams(location.search);
|
||||
params.set(QueryParams.order, value);
|
||||
history.push({ search: params.toString() });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="old-logs-explorer">
|
||||
<SpaceContainer
|
||||
split={<Divider type="vertical" />}
|
||||
align="center"
|
||||
direction="horizontal"
|
||||
>
|
||||
<LogsSearchFilter />
|
||||
<LogLiveTail />
|
||||
</SpaceContainer>
|
||||
|
||||
<LogsAggregate />
|
||||
|
||||
<Row gutter={20} wrap={false}>
|
||||
<LogsFilters />
|
||||
<Col flex={1} className="logs-col-container">
|
||||
<Row>
|
||||
<Col flex={1}>
|
||||
<Space align="baseline" direction="horizontal">
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
style={defaultSelectStyle}
|
||||
value={selectedViewModeOption}
|
||||
onChange={onChangeVeiwMode}
|
||||
>
|
||||
{viewModeOptionList.map((option) => (
|
||||
<Select.Option key={option.value}>{option.label}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{isFormatButtonVisible && (
|
||||
<Popover
|
||||
getPopupContainer={popupContainer}
|
||||
placement="right"
|
||||
content={renderPopoverContent}
|
||||
>
|
||||
<Button>Format</Button>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
style={defaultSelectStyle}
|
||||
defaultValue={order}
|
||||
onChange={handleChangeOrder}
|
||||
>
|
||||
{orderItems.map((item) => (
|
||||
<Select.Option key={item.enum}>{item.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col>
|
||||
<LogControls />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<LogsTable viewMode={viewMode} linesPerRow={linesPerRow} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<LogDetailedView />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default OldLogsExplorer;
|
||||
@@ -1,4 +0,0 @@
|
||||
.logs-col-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Space } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const SpaceContainer = styled(Space)`
|
||||
.ant-space-item:nth-child(1) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export default SpaceContainer;
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { ItemType } from 'antd/es/menu/hooks/useItems';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
|
||||
export type ViewModeOption = ItemType & {
|
||||
label: string;
|
||||
value: LogViewMode;
|
||||
};
|
||||
|
||||
export type SelectedLogViewData = {
|
||||
viewModeOptionList: ViewModeOption[];
|
||||
viewModeOption: ViewModeOption;
|
||||
viewMode: LogViewMode;
|
||||
handleViewModeChange: (s: LogViewMode) => void;
|
||||
handleViewModeOptionChange: ({ key }: { key: string }) => void;
|
||||
linesPerRow: number;
|
||||
handleLinesPerRowChange: (l: unknown) => void;
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
|
||||
import { OrderPreferenceItems, viewModeOptionList } from './config';
|
||||
|
||||
export const isLogViewMode = (value: unknown): value is LogViewMode =>
|
||||
typeof value === 'string' &&
|
||||
viewModeOptionList.some((option) => option.key === value);
|
||||
|
||||
export const getIdConditions = (
|
||||
idStart: string,
|
||||
idEnd: string,
|
||||
order: OrderPreferenceItems,
|
||||
): Record<string, string> => {
|
||||
const idConditions: Record<string, string> = {};
|
||||
|
||||
if (idStart && order === OrderPreferenceItems.ASC) {
|
||||
idConditions.idLt = idStart;
|
||||
} else if (idStart) {
|
||||
idConditions.idGt = idStart;
|
||||
}
|
||||
|
||||
if (idEnd && order === OrderPreferenceItems.ASC) {
|
||||
idConditions.idGt = idEnd;
|
||||
} else if (idEnd) {
|
||||
idConditions.idLt = idEnd;
|
||||
}
|
||||
|
||||
return idConditions;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { LogViewMode } from 'container/OptionsMenu/types';
|
||||
import {
|
||||
defaultLogsSelectedColumns,
|
||||
defaultTraceSelectedColumns,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { LogViewMode } from 'container/OptionsMenu/types';
|
||||
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { FormattingOptions } from 'providers/preferences/types';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { LogViewMode } from 'container/OptionsMenu/types';
|
||||
import {
|
||||
defaultLogsSelectedColumns,
|
||||
defaultOptionsQuery,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { LogViewMode } from 'container/OptionsMenu/types';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import {
|
||||
FormattingOptions,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { LogViewMode } from 'container/OptionsMenu/types';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
|
||||
export enum PreferenceMode {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import GetSearchFields from 'api/logs/GetSearchFields';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import AppActions from 'types/actions';
|
||||
import { SET_FIELDS } from 'types/actions/logs';
|
||||
|
||||
const IGNORED_SELECTED_FIELDS = ['timestamp'];
|
||||
|
||||
export const GetLogsFields =
|
||||
(): ((dispatch: Dispatch<AppActions>) => void) =>
|
||||
async (dispatch): Promise<void> => {
|
||||
const response = await GetSearchFields();
|
||||
if (response.payload) {
|
||||
dispatch({
|
||||
type: SET_FIELDS,
|
||||
payload: {
|
||||
interesting: response.payload.interesting,
|
||||
selected: response.payload.selected.filter(
|
||||
(field) => !IGNORED_SELECTED_FIELDS.includes(field.name),
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import GetLogs from 'api/logs/GetLogs';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import AppActions from 'types/actions';
|
||||
import { SET_LOADING, SET_LOGS } from 'types/actions/logs';
|
||||
import { Props } from 'types/api/logs/getLogs';
|
||||
|
||||
export const getLogs =
|
||||
(props: Props): ((dispatch: Dispatch<AppActions>) => void) =>
|
||||
async (dispatch): Promise<void> => {
|
||||
dispatch({
|
||||
type: SET_LOADING,
|
||||
payload: true,
|
||||
});
|
||||
|
||||
const response = await GetLogs(props);
|
||||
|
||||
if (response.payload) {
|
||||
dispatch({
|
||||
type: SET_LOGS,
|
||||
payload: response.payload,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: SET_LOGS,
|
||||
payload: [],
|
||||
});
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: SET_LOADING,
|
||||
payload: false,
|
||||
});
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import GetLogsAggregate from 'api/logs/GetLogsAggregate';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
SET_LOADING_AGGREGATE,
|
||||
SET_LOGS_AGGREGATE_SERIES,
|
||||
} from 'types/actions/logs';
|
||||
import { Props } from 'types/api/logs/getLogsAggregate';
|
||||
import { ILogsAggregate } from 'types/api/logs/logAggregate';
|
||||
|
||||
export const getLogsAggregate =
|
||||
(props: Props): ((dispatch: Dispatch<AppActions>) => void) =>
|
||||
async (dispatch): Promise<void> => {
|
||||
dispatch({
|
||||
type: SET_LOADING_AGGREGATE,
|
||||
payload: true,
|
||||
});
|
||||
|
||||
const response = await GetLogsAggregate(props);
|
||||
if (response.payload) {
|
||||
const convertedArray: ILogsAggregate[] = Object.values(response.payload).map(
|
||||
(data) => ({ ...data, time: new Date(data.timestamp / 1e6) }),
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: SET_LOGS_AGGREGATE_SERIES,
|
||||
payload: convertedArray,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: SET_LOGS_AGGREGATE_SERIES,
|
||||
payload: [],
|
||||
});
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: SET_LOADING_AGGREGATE,
|
||||
payload: false,
|
||||
});
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user