Compare commits

..

2 Commits

Author SHA1 Message Date
Abhi kumar
af1031a1cd Merge branch 'main' into chore/base-aggregation-breakdown 2026-04-25 01:35:39 +05:30
Abhi Kumar
1f602a216e chore: broke down drilldown navigate into a saperate hook 2026-04-23 13:46:19 +05:30
201 changed files with 5499 additions and 4698 deletions

View File

@@ -2474,164 +2474,6 @@ components:
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesNodeCondition:
enum:
- ready
- not_ready
- ""
type: string
InframonitoringtypesNodeRecord:
properties:
condition:
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
meta:
additionalProperties: {}
nullable: true
type: object
nodeCPU:
format: double
type: number
nodeCPUAllocatable:
format: double
type: number
nodeMemory:
format: double
type: number
nodeMemoryAllocatable:
format: double
type: number
nodeName:
type: string
notReadyNodeCount:
type: integer
readyNodeCount:
type: integer
required:
- nodeName
- condition
- readyNodeCount
- notReadyNodeCount
- nodeCPU
- nodeCPUAllocatable
- nodeMemory
- nodeMemoryAllocatable
- meta
type: object
InframonitoringtypesNodes:
properties:
endTimeBeforeRetention:
type: boolean
records:
items:
$ref: '#/components/schemas/InframonitoringtypesNodeRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
$ref: '#/components/schemas/InframonitoringtypesResponseType'
warning:
$ref: '#/components/schemas/Querybuildertypesv5QueryWarnData'
required:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesPodPhase:
enum:
- pending
- running
- succeeded
- failed
- unknown
- ""
type: string
InframonitoringtypesPodRecord:
properties:
failedPodCount:
type: integer
meta:
additionalProperties: {}
nullable: true
type: object
pendingPodCount:
type: integer
podAge:
format: int64
type: integer
podCPU:
format: double
type: number
podCPULimit:
format: double
type: number
podCPURequest:
format: double
type: number
podMemory:
format: double
type: number
podMemoryLimit:
format: double
type: number
podMemoryRequest:
format: double
type: number
podPhase:
$ref: '#/components/schemas/InframonitoringtypesPodPhase'
podUID:
type: string
runningPodCount:
type: integer
succeededPodCount:
type: integer
unknownPodCount:
type: integer
required:
- podUID
- podCPU
- podCPURequest
- podCPULimit
- podMemory
- podMemoryRequest
- podMemoryLimit
- podPhase
- pendingPodCount
- runningPodCount
- succeededPodCount
- failedPodCount
- unknownPodCount
- podAge
- meta
type: object
InframonitoringtypesPods:
properties:
endTimeBeforeRetention:
type: boolean
records:
items:
$ref: '#/components/schemas/InframonitoringtypesPodRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
$ref: '#/components/schemas/InframonitoringtypesResponseType'
warning:
$ref: '#/components/schemas/Querybuildertypesv5QueryWarnData'
required:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesPostableHosts:
properties:
end:
@@ -2658,58 +2500,6 @@ components:
- end
- limit
type: object
InframonitoringtypesPostableNodes:
properties:
end:
format: int64
type: integer
filter:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
nullable: true
type: array
limit:
type: integer
offset:
type: integer
orderBy:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
start:
format: int64
type: integer
required:
- start
- end
- limit
type: object
InframonitoringtypesPostablePods:
properties:
end:
format: int64
type: integer
filter:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
nullable: true
type: array
limit:
type: integer
offset:
type: integer
orderBy:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
start:
format: int64
type: integer
required:
- start
- end
- limit
type: object
InframonitoringtypesRequiredMetricsCheck:
properties:
missingMetrics:
@@ -10437,9 +10227,7 @@ paths:
five metrics, and pagination via offset/limit. The response type is ''list''
for the default host.name grouping or ''grouped_list'' for custom groupBy
keys. Also reports missing required metrics and whether the requested time
range falls before the data retention boundary. Numeric metric fields (cpu,
memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available
for that field; frontends should render ''—'' rather than the literal value.'
range falls before the data retention boundary.'
operationId: ListHosts
requestBody:
content:
@@ -10493,151 +10281,6 @@ paths:
summary: List Hosts for Infra Monitoring
tags:
- inframonitoring
/api/v2/infra_monitoring/nodes:
post:
deprecated: false
description: 'Returns a paginated list of Kubernetes nodes with key metrics:
CPU usage, CPU allocatable, memory working set, memory allocatable, and per-group
readyNodeCount / notReadyNodeCount derived from each node''s latest k8s.node.condition_ready
value in the window. Each node includes metadata attributes (k8s.node.uid,
k8s.cluster.name). The response type is ''list'' for the default k8s.node.name
grouping (each row is one node with its current condition string: ready /
not_ready / '''') or ''grouped_list'' for custom groupBy keys (each row aggregates
nodes in the group with readyNodeCount and notReadyNodeCount; condition stays
empty). Supports filtering via a filter expression, custom groupBy, ordering
by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via
offset/limit. Also reports missing required metrics and whether the requested
time range falls before the data retention boundary. Numeric metric fields
(nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1
as a sentinel when no data is available for that field; frontends should render
''—'' rather than the literal value.'
operationId: ListNodes
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InframonitoringtypesPostableNodes'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/InframonitoringtypesNodes'
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
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List Nodes for Infra Monitoring
tags:
- inframonitoring
/api/v2/infra_monitoring/pods:
post:
deprecated: false
description: 'Returns a paginated list of Kubernetes pods with key metrics:
CPU usage, CPU request/limit utilization, memory working set, memory request/limit
utilization, current pod phase (pending/running/succeeded/failed/unknown),
and pod age (ms since start time). Each pod includes metadata attributes (namespace,
node, workload owner such as deployment/statefulset/daemonset/job/cronjob,
cluster). Supports filtering via a filter expression, custom groupBy to aggregate
pods by any attribute, ordering by any of the six metrics (cpu, cpu_request,
cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit.
The response type is ''list'' for the default k8s.pod.uid grouping (each row
is one pod with its current phase) or ''grouped_list'' for custom groupBy
keys (each row aggregates pods in the group with per-phase counts: pendingPodCount,
runningPodCount, succeededPodCount, failedPodCount, unknownPodCount derived
from each pod''s latest phase in the window). Also reports missing required
metrics and whether the requested time range falls before the data retention
boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory,
podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no
data is available for that field; frontends should render ''—'' rather than
the literal value.'
operationId: ListPods
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InframonitoringtypesPostablePods'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/InframonitoringtypesPods'
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
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List Pods for Infra Monitoring
tags:
- inframonitoring
/api/v2/livez:
get:
deprecated: false

View File

@@ -313,6 +313,14 @@
"name": "react-redux",
"message": "[State mgmt] react-redux is deprecated. Migrate to Zustand, nuqs, or react-query."
},
{
"name": "xstate",
"message": "[State mgmt] xstate is deprecated. Migrate to Zustand or react-query."
},
{
"name": "@xstate/react",
"message": "[State mgmt] @xstate/react is deprecated. Migrate to Zustand or react-query."
},
{
"name": "react",
"importNames": [

View File

@@ -63,6 +63,7 @@
"@visx/shape": "3.5.0",
"@visx/tooltip": "3.3.0",
"@vitejs/plugin-react": "5.1.4",
"@xstate/react": "^3.0.0",
"ansi-to-html": "0.7.2",
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
@@ -145,6 +146,7 @@
"vite": "npm:rolldown-vite@7.3.1",
"vite-plugin-html": "3.2.2",
"web-vitals": "^0.2.4",
"xstate": "^4.31.0",
"zod": "4.3.6",
"zustand": "5.0.11"
},

View File

@@ -16,6 +16,5 @@
"roles": "Roles",
"role_details": "Role Details",
"members": "Members",
"service_accounts": "Service Accounts",
"mcp_server": "MCP Server"
"service_accounts": "Service Accounts"
}

View File

@@ -53,6 +53,5 @@
"METER": "SigNoz | Meter",
"ROLES_SETTINGS": "SigNoz | Roles",
"MEMBERS_SETTINGS": "SigNoz | Members",
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
"MCP_SERVER": "SigNoz | MCP Server"
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts"
}

View File

@@ -16,6 +16,5 @@
"roles": "Roles",
"role_details": "Role Details",
"members": "Members",
"service_accounts": "Service Accounts",
"mcp_server": "MCP Server"
"service_accounts": "Service Accounts"
}

View File

@@ -76,6 +76,5 @@
"METER": "SigNoz | Meter",
"ROLES_SETTINGS": "SigNoz | Roles",
"MEMBERS_SETTINGS": "SigNoz | Members",
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
"MCP_SERVER": "SigNoz | MCP Server"
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts"
}

View File

@@ -13,11 +13,7 @@ import type {
import type {
InframonitoringtypesPostableHostsDTO,
InframonitoringtypesPostableNodesDTO,
InframonitoringtypesPostablePodsDTO,
ListHosts200,
ListNodes200,
ListPods200,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
@@ -25,7 +21,7 @@ import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns a paginated list of hosts with key infrastructure metrics: CPU usage (%), memory usage (%), I/O wait (%), disk usage (%), and 15-minute load average. Each host includes its current status (active/inactive based on metrics reported in the last 10 minutes) and metadata attributes (e.g., os.type). Supports filtering via a filter expression, filtering by host status, custom groupBy to aggregate hosts by any attribute, ordering by any of the five metrics, and pagination via offset/limit. The response type is 'list' for the default host.name grouping or 'grouped_list' for custom groupBy keys. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available for that field; frontends should render '—' rather than the literal value.
* Returns a paginated list of hosts with key infrastructure metrics: CPU usage (%), memory usage (%), I/O wait (%), disk usage (%), and 15-minute load average. Each host includes its current status (active/inactive based on metrics reported in the last 10 minutes) and metadata attributes (e.g., os.type). Supports filtering via a filter expression, filtering by host status, custom groupBy to aggregate hosts by any attribute, ordering by any of the five metrics, and pagination via offset/limit. The response type is 'list' for the default host.name grouping or 'grouped_list' for custom groupBy keys. Also reports missing required metrics and whether the requested time range falls before the data retention boundary.
* @summary List Hosts for Infra Monitoring
*/
export const listHosts = (
@@ -108,171 +104,3 @@ export const useListHosts = <
return useMutation(mutationOptions);
};
/**
* Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, and per-group readyNodeCount / notReadyNodeCount derived from each node's latest k8s.node.condition_ready value in the window. Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / '') or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group with readyNodeCount and notReadyNodeCount; condition stays empty). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field; frontends should render '—' rather than the literal value.
* @summary List Nodes for Infra Monitoring
*/
export const listNodes = (
inframonitoringtypesPostableNodesDTO: BodyType<InframonitoringtypesPostableNodesDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListNodes200>({
url: `/api/v2/infra_monitoring/nodes`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: inframonitoringtypesPostableNodesDTO,
signal,
});
};
export const getListNodesMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listNodes>>,
TError,
{ data: BodyType<InframonitoringtypesPostableNodesDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof listNodes>>,
TError,
{ data: BodyType<InframonitoringtypesPostableNodesDTO> },
TContext
> => {
const mutationKey = ['listNodes'];
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 listNodes>>,
{ data: BodyType<InframonitoringtypesPostableNodesDTO> }
> = (props) => {
const { data } = props ?? {};
return listNodes(data);
};
return { mutationFn, ...mutationOptions };
};
export type ListNodesMutationResult = NonNullable<
Awaited<ReturnType<typeof listNodes>>
>;
export type ListNodesMutationBody =
BodyType<InframonitoringtypesPostableNodesDTO>;
export type ListNodesMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List Nodes for Infra Monitoring
*/
export const useListNodes = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listNodes>>,
TError,
{ data: BodyType<InframonitoringtypesPostableNodesDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof listNodes>>,
TError,
{ data: BodyType<InframonitoringtypesPostableNodesDTO> },
TContext
> => {
const mutationOptions = getListNodesMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed/unknown), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts: pendingPodCount, runningPodCount, succeededPodCount, failedPodCount, unknownPodCount derived from each pod's latest phase in the window). Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field; frontends should render '—' rather than the literal value.
* @summary List Pods for Infra Monitoring
*/
export const listPods = (
inframonitoringtypesPostablePodsDTO: BodyType<InframonitoringtypesPostablePodsDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListPods200>({
url: `/api/v2/infra_monitoring/pods`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: inframonitoringtypesPostablePodsDTO,
signal,
});
};
export const getListPodsMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listPods>>,
TError,
{ data: BodyType<InframonitoringtypesPostablePodsDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof listPods>>,
TError,
{ data: BodyType<InframonitoringtypesPostablePodsDTO> },
TContext
> => {
const mutationKey = ['listPods'];
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 listPods>>,
{ data: BodyType<InframonitoringtypesPostablePodsDTO> }
> = (props) => {
const { data } = props ?? {};
return listPods(data);
};
return { mutationFn, ...mutationOptions };
};
export type ListPodsMutationResult = NonNullable<
Awaited<ReturnType<typeof listPods>>
>;
export type ListPodsMutationBody =
BodyType<InframonitoringtypesPostablePodsDTO>;
export type ListPodsMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List Pods for Infra Monitoring
*/
export const useListPods = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listPods>>,
TError,
{ data: BodyType<InframonitoringtypesPostablePodsDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof listPods>>,
TError,
{ data: BodyType<InframonitoringtypesPostablePodsDTO> },
TContext
> => {
const mutationOptions = getListPodsMutationOptions(options);
return useMutation(mutationOptions);
};

View File

@@ -3243,180 +3243,6 @@ export interface InframonitoringtypesHostsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export enum InframonitoringtypesNodeConditionDTO {
ready = 'ready',
not_ready = 'not_ready',
'' = '',
}
/**
* @nullable
*/
export type InframonitoringtypesNodeRecordDTOMeta = {
[key: string]: unknown;
} | null;
export interface InframonitoringtypesNodeRecordDTO {
condition: InframonitoringtypesNodeConditionDTO;
/**
* @type object
* @nullable true
*/
meta: InframonitoringtypesNodeRecordDTOMeta;
/**
* @type number
* @format double
*/
nodeCPU: number;
/**
* @type number
* @format double
*/
nodeCPUAllocatable: number;
/**
* @type number
* @format double
*/
nodeMemory: number;
/**
* @type number
* @format double
*/
nodeMemoryAllocatable: number;
/**
* @type string
*/
nodeName: string;
/**
* @type integer
*/
notReadyNodeCount: number;
/**
* @type integer
*/
readyNodeCount: number;
}
export interface InframonitoringtypesNodesDTO {
/**
* @type boolean
*/
endTimeBeforeRetention: boolean;
/**
* @type array
* @nullable true
*/
records: InframonitoringtypesNodeRecordDTO[] | null;
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
total: number;
type: InframonitoringtypesResponseTypeDTO;
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export enum InframonitoringtypesPodPhaseDTO {
pending = 'pending',
running = 'running',
succeeded = 'succeeded',
failed = 'failed',
unknown = 'unknown',
'' = '',
}
/**
* @nullable
*/
export type InframonitoringtypesPodRecordDTOMeta = {
[key: string]: unknown;
} | null;
export interface InframonitoringtypesPodRecordDTO {
/**
* @type integer
*/
failedPodCount: number;
/**
* @type object
* @nullable true
*/
meta: InframonitoringtypesPodRecordDTOMeta;
/**
* @type integer
*/
pendingPodCount: number;
/**
* @type integer
* @format int64
*/
podAge: number;
/**
* @type number
* @format double
*/
podCPU: number;
/**
* @type number
* @format double
*/
podCPULimit: number;
/**
* @type number
* @format double
*/
podCPURequest: number;
/**
* @type number
* @format double
*/
podMemory: number;
/**
* @type number
* @format double
*/
podMemoryLimit: number;
/**
* @type number
* @format double
*/
podMemoryRequest: number;
podPhase: InframonitoringtypesPodPhaseDTO;
/**
* @type string
*/
podUID: string;
/**
* @type integer
*/
runningPodCount: number;
/**
* @type integer
*/
succeededPodCount: number;
/**
* @type integer
*/
unknownPodCount: number;
}
export interface InframonitoringtypesPodsDTO {
/**
* @type boolean
*/
endTimeBeforeRetention: boolean;
/**
* @type array
* @nullable true
*/
records: InframonitoringtypesPodRecordDTO[] | null;
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
total: number;
type: InframonitoringtypesResponseTypeDTO;
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesPostableHostsDTO {
/**
* @type integer
@@ -3445,62 +3271,6 @@ export interface InframonitoringtypesPostableHostsDTO {
start: number;
}
export interface InframonitoringtypesPostableNodesDTO {
/**
* @type integer
* @format int64
*/
end: number;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array
* @nullable true
*/
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
/**
* @type integer
*/
limit: number;
/**
* @type integer
*/
offset?: number;
orderBy?: Querybuildertypesv5OrderByDTO;
/**
* @type integer
* @format int64
*/
start: number;
}
export interface InframonitoringtypesPostablePodsDTO {
/**
* @type integer
* @format int64
*/
end: number;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array
* @nullable true
*/
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
/**
* @type integer
*/
limit: number;
/**
* @type integer
*/
offset?: number;
orderBy?: Querybuildertypesv5OrderByDTO;
/**
* @type integer
* @format int64
*/
start: number;
}
export interface InframonitoringtypesRequiredMetricsCheckDTO {
/**
* @type array
@@ -7337,22 +7107,6 @@ export type ListHosts200 = {
status: string;
};
export type ListNodes200 = {
data: InframonitoringtypesNodesDTO;
/**
* @type string
*/
status: string;
};
export type ListPods200 = {
data: InframonitoringtypesPodsDTO;
/**
* @type string
*/
status: string;
};
export type Livez200 = {
data: FactoryResponseDTO;
/**

View File

@@ -0,0 +1,25 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import {
GlobalConfigData,
GlobalConfigDataProps,
} from 'types/api/globalConfig/types';
const getGlobalConfig = async (): Promise<
SuccessResponseV2<GlobalConfigData>
> => {
try {
const response = await axios.get<GlobalConfigDataProps>(`/global/config`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getGlobalConfig;

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" height="34" viewBox="0 0 131 34">
<path fill="currentColor" d="M.36 8.6h16.7v5.6H6.04c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h11.02v5.6h-5.33c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h4.98c.2 0 .35-.15.35-.35V25.4h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34v-5.6h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34V3.35c0-.2-.16-.35-.35-.35H.36c-.2 0-.36.16-.36.35v4.9c0 .2.16.35.36.35ZM44.41 14.7c-.5-.5-1.1-.9-1.76-1.18a5.62 5.62 0 0 0-4.6.17c-.73.37-1.32.91-1.75 1.62h-.17V8.59H34.1v16.83h2.04v-1.81h.17c.21.36.47.67.77.94.31.25.65.48 1.01.67.37.18.77.31 1.18.39a6.2 6.2 0 0 0 3.39-.24 5.36 5.36 0 0 0 3.02-3.1c.29-.75.44-1.62.44-2.6v-.47c0-.96-.16-1.83-.47-2.58-.3-.75-.7-1.4-1.23-1.9v-.01Zm-5.87.66a3.9 3.9 0 0 1 4.34.84c.36.35.64.8.83 1.3.2.5.3 1.07.3 1.7v.47c0 .64-.1 1.23-.3 1.74a3.75 3.75 0 0 1-2.06 2.15 4.27 4.27 0 0 1-3.12-.03 3.86 3.86 0 0 1-2.09-2.2c-.2-.52-.3-1.11-.3-1.75v-.29c0-.62.1-1.2.3-1.7v-.01c.21-.53.5-.99.84-1.36.36-.37.78-.66 1.26-.86ZM97.04 8.59H95v4.86h-2.94v1.86H95v8.17c0 .56.17 1.03.53 1.4.37.35.84.54 1.4.54h4.18v-1.87h-3.5c-.2 0-.33-.05-.43-.15-.1-.1-.14-.27-.14-.49v-7.6h4.65v-1.86h-4.65V8.59ZM114.61 15a5.48 5.48 0 0 0-1.8-1.33 5.6 5.6 0 0 0-2.57-.56 6.17 6.17 0 0 0-4.26 1.7 5.6 5.6 0 0 0-1.72 4.2v.57c0 .9.15 1.75.44 2.5a5.58 5.58 0 0 0 5.5 3.67c1.55 0 2.8-.35 3.72-1.04a5.35 5.35 0 0 0 1.91-2.73l.03-.07-1.94-.52-.02.07c-.11.33-.27.64-.46.94-.17.27-.4.52-.7.74-.28.22-.63.39-1.04.51-.41.13-.9.19-1.46.19a3.8 3.8 0 0 1-2.84-1.05 4.07 4.07 0 0 1-1.1-2.7h9.68v-1.6c0-.54-.11-1.12-.34-1.75a5.04 5.04 0 0 0-1.03-1.74Zm-8.25 3.21a3.8 3.8 0 0 1 1.22-2.25 4.19 4.19 0 0 1 3.99-.7c.44.16.83.38 1.17.66.34.27.62.62.82 1.02.21.38.34.8.38 1.27h-7.58ZM129.09 14.42a4.47 4.47 0 0 0-3.37-1.3c-.93 0-1.73.2-2.4.59-.64.39-1.15.97-1.52 1.74h-.17v-2h-2.04v11.97h2.04v-6.23c0-1.26.32-2.28.95-3.02a3.31 3.31 0 0 1 2.65-1.14c.94 0 1.7.3 2.24.9.56.6.83 1.52.83 2.74v6.75h2.04v-7.13c0-1.71-.42-3.02-1.25-3.87ZM88.1 15a5.48 5.48 0 0 0-1.78-1.33 5.6 5.6 0 0 0-2.58-.56 6.17 6.17 0 0 0-4.27 1.7 5.59 5.59 0 0 0-1.71 4.2v.56c0 .92.14 1.76.44 2.51a5.6 5.6 0 0 0 5.5 3.67c1.55 0 2.8-.35 3.72-1.04a5.36 5.36 0 0 0 1.91-2.73l.03-.07-1.94-.52-.03.07c-.1.32-.26.64-.45.94-.17.27-.4.52-.7.74-.29.21-.64.39-1.05.51-.4.12-.9.19-1.45.19a3.8 3.8 0 0 1-2.85-1.05 4.07 4.07 0 0 1-1.09-2.7h9.68v-1.61c0-.53-.12-1.12-.34-1.74A5.03 5.03 0 0 0 88.1 15Zm-8.24 3.21a3.83 3.83 0 0 1 1.22-2.25 4.2 4.2 0 0 1 3.99-.7c.44.16.83.38 1.16.66.35.27.62.62.83 1.02.2.38.33.8.37 1.27h-7.57ZM73.65 19.42a6.11 6.11 0 0 0-3.23-1.02 6.63 6.63 0 0 1-2.68-.58c-.47-.3-.7-.7-.7-1.25 0-.27.08-.5.21-.7.14-.2.33-.38.56-.52a4.05 4.05 0 0 1 1.78-.42c.85 0 1.54.21 2.06.63.53.41.83 1 .91 1.73l.01.1 1.95-.47-.01-.07c-.07-.45-.22-.91-.45-1.36a3.46 3.46 0 0 0-.94-1.2 4.6 4.6 0 0 0-1.52-.84 6.05 6.05 0 0 0-2.1-.34c-.58 0-1.13.07-1.65.22-.52.14-1 .36-1.43.65-.41.3-.74.66-.99 1.1a2.9 2.9 0 0 0-.37 1.49v.14c0 1.06.38 1.87 1.14 2.42a6.2 6.2 0 0 0 3.24.97c1.18.08 2.04.26 2.56.54.5.28.75.72.75 1.36 0 .6-.25 1.06-.78 1.4-.52.32-1.22.48-2.1.48a3.68 3.68 0 0 1-2.46-.79 3.13 3.13 0 0 1-1.04-2.14v-.09l-1.93.46h-.02v.07a4.4 4.4 0 0 0 3.1 4c.7.24 1.52.36 2.46.36.7 0 1.35-.09 1.93-.27.6-.16 1.12-.4 1.53-.72A3.38 3.38 0 0 0 74.8 22v-.14c0-1.07-.39-1.9-1.14-2.44ZM60.25 23.4c-.1-.1-.14-.27-.14-.49v-9.46h-2.05v1.85h-.16a3.78 3.78 0 0 0-1.61-1.63 4.62 4.62 0 0 0-2.26-.56c-.77 0-1.5.14-2.19.41a5.27 5.27 0 0 0-3.02 3.12c-.29.75-.44 1.63-.44 2.6v.38c0 .99.15 1.87.44 2.63.3.75.7 1.4 1.2 1.93a5.48 5.48 0 0 0 4.05 1.57c.8 0 1.51-.2 2.2-.58.69-.38 1.24-.97 1.63-1.75h.16v.06c0 .56.18 1.03.53 1.4.37.35.85.54 1.41.54h1.36v-1.87h-.68c-.2 0-.34-.05-.43-.15Zm-4.46.13c-.46.2-.97.3-1.52.3a3.68 3.68 0 0 1-2.75-1.09 4.42 4.42 0 0 1-1.05-3.12v-.38c0-.62.1-1.2.29-1.71a3.65 3.65 0 0 1 5-2.17c.47.2.88.49 1.21.86.35.37.62.83.8 1.36.2.5.3 1.08.3 1.7v.3c0 .63-.1 1.23-.3 1.76-.18.5-.45.96-.78 1.33-.33.37-.73.66-1.2.86Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,13 +0,0 @@
<svg width="109" height="24" viewBox="0 0 109 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_125_22125)">
<path d="M0 -2.08616e-07V24H17.9352C22.9911 24 26.0999 21.0858 26.0999 17.04V6.96C26.0999 2.91432 22.9911 -2.08616e-07 17.9352 -2.08616e-07H0ZM6.76413 5.82864H19.1992V18.1714H6.76413V5.82864Z" fill="currentColor"/>
<path d="M46.7659 18.6172H35.0824V14.16H46.7659V18.6172ZM46.595 5.38296V9.5658H35.0824V5.38296H46.595ZM50.2846 12.1373V11.5886C52.5734 10.5258 53.7008 8.8458 53.7008 6.13728C53.7008 2.64012 50.9337 0.000116183 45.5361 0.000116183H28.3184V24H45.7752C51.1728 24 53.9399 21.8401 53.9399 18.0685C53.9399 15.0172 52.6418 13.2001 50.2846 12.1373Z" fill="currentColor"/>
<path d="M62.397 18.1714H74.8319V5.82864H62.397V18.1714ZM63.6609 24C58.6049 24 55.4961 21.0858 55.4961 17.04V6.96012C55.4961 2.91432 58.6049 0.000116183 63.6609 0.000116183H73.568C78.6238 0.000116183 81.7326 2.91432 81.7326 6.96012V17.04C81.7326 21.0858 78.6238 24 73.568 24H63.6609Z" fill="currentColor"/>
<path d="M101.66 15.12L90.8995 14.3658C85.5361 13.9886 83.418 11.1772 83.418 7.47432V6.96012C83.418 2.91432 86.5266 0.000116183 91.5827 0.000116183H100.157C105.214 0.000116183 108.323 2.91432 108.323 6.96012V7.98864H101.968V5.14284H90.2504V8.43432L100.601 9.18864C105.999 9.56568 108.493 12.8572 108.493 16.5257V17.04C108.493 20.7428 105.384 24 100.328 24H91.5827C86.5266 24 83.418 20.7428 83.418 17.04V16.0115H89.7722V18.8572H101.66V15.12Z" fill="currentColor"/>
</g>
<defs>
<clipPath id="clip0_125_22125">
<rect width="108.494" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1 +0,0 @@
<svg width='234' height='42' viewBox='0 0 234 42' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M18 30H6V18H18V30Z' fill='#CFCECD'/><path d='M18 12H6V30H18V12ZM24 36H0V6H24V36Z' fill='#656363'/><path d='M48 30H36V18H48V30Z' fill='#CFCECD'/><path d='M36 30H48V12H36V30ZM54 36H36V42H30V6H54V36Z' fill='#656363'/><path d='M84 24V30H66V24H84Z' fill='#CFCECD'/><path d='M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z' fill='#656363'/><path d='M108 36H96V18H108V36Z' fill='#CFCECD'/><path d='M108 12H96V36H90V6H108V12ZM114 36H108V12H114V36Z' fill='#656363'/><path d='M144 30H126V18H144V30Z' fill='#CFCECD'/><path d='M144 12H126V30H144V36H120V6H144V12Z' fill='#211E1E'/><path d='M168 30H156V18H168V30Z' fill='#CFCECD'/><path d='M168 12H156V30H168V12ZM174 36H150V6H174V36Z' fill='#211E1E'/><path d='M198 30H186V18H198V30Z' fill='#CFCECD'/><path d='M198 12H186V30H198V12ZM204 36H180V6H198V0H204V36Z' fill='#211E1E'/><path d='M234 24V30H216V24H234Z' fill='#CFCECD'/><path d='M216 12V18H228V12H216ZM234 24H216V30H234V36H210V6H234V24Z' fill='#211E1E'/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -74,7 +74,7 @@
width: 100%;
height: 100%;
background: radial-gradient(circle, var(--l1-foreground) 10%, transparent 0);
background: radial-gradient(circle, #fff 10%, transparent 0);
background-size: 12px 12px;
opacity: 1;

View File

@@ -99,6 +99,36 @@
}
}
.lightMode {
.auth-error-container {
.error-content {
&__error-code {
color: var(--l2-foreground);
}
&__error-message {
color: var(--l1-foreground);
}
&__message-badge-label-text {
color: var(--l2-foreground);
}
&__message-item {
color: var(--l1-foreground);
&::before {
background: var(--l3-background);
}
}
&__scroll-hint-text {
color: var(--l2-foreground);
}
}
}
}
@keyframes horizontal-shaking {
0% {
transform: translateX(0);

View File

@@ -87,3 +87,23 @@
background: var(--l3-background);
flex-shrink: 0;
}
.lightMode {
.auth-footer-content {
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.08);
}
.auth-footer-icon {
filter: brightness(0) saturate(100%) invert(25%) sepia(8%) saturate(518%)
hue-rotate(192deg) brightness(80%) contrast(95%);
opacity: 0.9;
}
.auth-footer-text {
color: var(--text-neutral-light-200);
}
.auth-footer-link-icon {
color: var(--text-neutral-light-100);
}
}

View File

@@ -143,3 +143,28 @@
}
}
}
.lightMode {
.bg-dot-pattern {
background: radial-gradient(
circle,
var(--l3-background) 1px,
transparent 1px
);
background-size: 12px 12px;
}
.auth-page-gradient {
background: radial-gradient(
ellipse at center top,
color-mix(in srgb, var(--primary-background) 12%, transparent) 0%,
transparent 60%
);
opacity: 0.8;
filter: blur(200px);
@media (min-width: 768px) {
filter: blur(300px);
}
}
}

View File

@@ -41,6 +41,7 @@
justify-content: center;
align-items: center;
border: 1px solid var(--l1-border);
background: linear-gradient(0deg, transparent 0%, transparent 100%), #0b0c0e;
.ant-card-body {
height: 100%;
@@ -237,7 +238,18 @@
height: 2px;
bottom: 0;
left: 0;
background-color: var(--l1-background);
color: var(--l1-foreground);
background-color: var(--l1-foreground);
}
}
.lightMode {
.celery-task-graph-grid-container {
.celery-task-graph-worker-count {
background: unset;
}
}
.configure-option-Info {
border: 1px dashed var(--bg-robin-400);
}
}

View File

@@ -32,7 +32,6 @@ function CreateServiceAccountModal(): JSX.Element {
SA_QUERY_PARAMS.CREATE_SA,
parseAsBoolean.withDefault(false),
);
const [, setSelectedAccountId] = useQueryState(SA_QUERY_PARAMS.ACCOUNT);
const { showErrorModal, isErrorModalVisible } = useErrorModal();
@@ -51,12 +50,11 @@ function CreateServiceAccountModal(): JSX.Element {
const { mutate: createServiceAccount, isLoading: isSubmitting } =
useCreateServiceAccount({
mutation: {
onSuccess: async (response) => {
onSuccess: async () => {
toast.success('Service account created successfully');
reset();
await setIsOpen(null);
await invalidateListServiceAccounts(queryClient);
await setSelectedAccountId(response.data.id);
},
onError: (err) => {
const errMessage = convertToApiError(
@@ -69,7 +67,7 @@ function CreateServiceAccountModal(): JSX.Element {
function handleClose(): void {
reset();
void setIsOpen(null);
setIsOpen(null);
}
function handleCreate(values: FormValues): void {

View File

@@ -77,11 +77,11 @@
width: 280px;
&::placeholder {
color: var(--l1-foreground);
color: white;
}
&:focus::placeholder {
color: rgba($color: var(--l1-foreground), $alpha: 0.4);
color: rgba($color: #ffffff, $alpha: 0.4);
}
}
}
@@ -113,6 +113,42 @@
}
}
.lightMode {
.time-options-container {
.time-options-item {
&.active {
background-color: rgba($color: #ffffff, $alpha: 0.2);
&:hover {
cursor: pointer;
background-color: rgba($color: #ffffff, $alpha: 0.3);
}
}
&:hover {
cursor: pointer;
background-color: rgba($color: #ffffff, $alpha: 0.3);
}
}
}
.timeSelection-input {
display: flex;
gap: 8px;
align-items: center;
padding: 4px 8px;
padding-left: 0px !important;
input::placeholder {
color: var(---bg-ink-300);
}
input:focus::placeholder {
color: rgba($color: #000000, $alpha: 0.4);
}
}
}
.date-time-popover__footer {
border-top: 1px solid var(--l1-border);
padding: 8px 14px;
@@ -264,3 +300,34 @@
background: color-mix(in srgb, var(--bg-robin-200) 8%, transparent);
}
}
.lightMode {
.timezone-container {
.timezone {
background: rgb(179 179 179 / 15%);
&__icon {
stroke: var(--l1-foreground);
}
}
}
.custom-time-picker {
.timeSelection-input {
&:hover {
border-color: var(--l1-border) !important;
}
}
}
.timezone-badge {
background: rgb(179 179 179 / 15%);
}
.time-input-suffix-icon-badge {
background: rgb(179 179 179 / 15%);
&:hover {
background: rgb(179 179 179 / 20%);
}
}
}

View File

@@ -129,3 +129,20 @@ $item-spacing: 8px;
width: 15px;
}
}
.lightMode {
.timezone-picker {
&__search {
.search-icon {
stroke: var(--l1-foreground);
}
}
}
.timezone-name-wrapper {
&__selected-icon {
.check-icon {
stroke: var(--l1-foreground);
}
}
}
}

View File

@@ -1,5 +1,9 @@
.dropdown-button {
color: var(--l1-foreground);
color: #fff;
}
.dropdown-button--dark {
color: #000;
}
.dropdown-icon {

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
import { EllipsisOutlined } from '@ant-design/icons';
import { Button, Dropdown, MenuProps } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import './DropDown.styles.scss';
@@ -11,6 +12,8 @@ function DropDown({
element: JSX.Element[];
onDropDownItemClick?: MenuProps['onClick'];
}): JSX.Element {
const isDarkMode = useIsDarkMode();
const items: MenuProps['items'] = element.map(
(e: JSX.Element, index: number) => ({
label: e,
@@ -32,7 +35,7 @@ function DropDown({
>
<Button
type="link"
className={`dropdown-button`}
className={!isDarkMode ? 'dropdown-button--dark' : 'dropdown-button'}
onClick={(e): void => {
e.preventDefault();
setDdOpen(true);

View File

@@ -10,7 +10,6 @@ import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
@@ -47,7 +46,6 @@ import {
TextSelect,
X,
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { AppState } from 'store/reducers';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
@@ -81,10 +79,6 @@ function LogDetailInner({
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
const [isFilterVisible, setIsFilterVisible] = useState<boolean>(false);
const { featureFlags } = useAppContext();
const isBodyJsonQueryEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.BODY_JSON_ENABLED)
?.active || false;
const [filters, setFilters] = useState<TagFilter | null>(null);
const [isEdit, setIsEdit] = useState<boolean>(false);
@@ -214,29 +208,11 @@ function LogDetailInner({
}
};
const logBody = useMemo(() => {
if (!isBodyJsonQueryEnabled) {
return log?.body || '';
}
try {
const json = JSON.parse(log?.body || '');
if (typeof json?.message === 'string' && json.message !== '') {
return json.message;
}
return log?.body || '';
} catch (error) {
return log?.body || '';
}
}, [isBodyJsonQueryEnabled, log?.body]);
const htmlBody = useMemo(
() => ({
__html: getSanitizedLogBody(logBody || '', { shouldEscapeHtml: true }),
__html: getSanitizedLogBody(log?.body || '', { shouldEscapeHtml: true }),
}),
[logBody],
[log?.body],
);
const handleJSONCopy = (): void => {
@@ -442,7 +418,7 @@ function LogDetailInner({
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip
title={removeEscapeCharacters(logBody)}
title={removeEscapeCharacters(log?.body)}
placement="left"
mouseLeaveDelay={0}
>

View File

@@ -196,3 +196,17 @@
opacity: 0.5;
}
}
.lightMode {
.members-table {
.ant-table-tbody {
> tr.members-table-row--tinted > td {
background: rgba(0, 0, 0, 0.015);
}
> tr:hover > td {
background: rgba(0, 0, 0, 0.03) !important;
}
}
}
}

View File

@@ -167,3 +167,22 @@
padding-right: 8px;
}
}
.lightMode {
.config-btn {
&.missing-config-btn {
background: var(--bg-amber-100);
color: var(--bg-amber-500);
&:hover {
color: var(--bg-amber-600) !important;
}
}
.missing-config-btn {
.config-btn-content {
border-right: 1px solid var(--bg-amber-600);
}
}
}
}

View File

@@ -16,7 +16,7 @@ $custom-border-color: #2c3044;
}
.ant-select-selection-placeholder {
color: color-mix(in srgb, var(--l2-foreground) 45%, transparent);
color: color-mix(in srgb, var(--border) 45%, transparent);
}
// Base styles are for dark mode
@@ -48,6 +48,10 @@ $custom-border-color: #2c3044;
visibility: visible !important;
pointer-events: none;
z-index: 2;
.lightMode & {
color: rgba(0, 0, 0, 0.85) !important;
}
}
&.ant-select-focused .ant-select-selection-placeholder {
@@ -63,6 +67,10 @@ $custom-border-color: #2c3044;
color: var(--l2-foreground);
z-index: 1;
pointer-events: none;
.lightMode & {
color: rgba(0, 0, 0, 0.85);
}
}
.ant-select-selector {
@@ -106,7 +114,7 @@ $custom-border-color: #2c3044;
}
.ant-select-selection-placeholder {
color: color-mix(in srgb, var(--l2-foreground) 45%, transparent);
color: color-mix(in srgb, var(--border) 45%, transparent);
}
// Customize tags in multiselect (dark mode by default)
@@ -209,7 +217,7 @@ $custom-border-color: #2c3044;
.empty-message {
padding: 12px;
text-align: center;
color: color-mix(in srgb, var(--l1-foreground) 45%, transparent);
color: color-mix(in srgb, var(--border) 45%, transparent);
}
}
@@ -567,7 +575,7 @@ $custom-border-color: #2c3044;
.empty-message {
padding: 12px;
text-align: center;
color: color-mix(in srgb, var(--l1-foreground) 45%, transparent);
color: color-mix(in srgb, var(--border) 45%, transparent);
}
.status-message {
@@ -975,6 +983,10 @@ $custom-border-color: #2c3044;
transition:
opacity 0.2s ease,
visibility 0.2s ease;
.lightMode & {
color: rgba(0, 0, 0, 0.85);
}
}
&:focus-within .all-text {

View File

@@ -249,6 +249,57 @@
}
}
.lightMode {
.query-aggregation-container {
.aggregation-container {
.query-aggregation-select-container {
.query-aggregation-select-editor {
.cm-editor {
.cm-tooltip-autocomplete {
background: var(--l1-background) !important;
border: 1px solid var(--l1-border) !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
backdrop-filter: none;
ul {
li {
&:hover,
&[aria-selected='true'] {
background: var(--l3-background) !important;
}
}
}
}
.cm-line {
::-moz-selection {
background: var(--l2-background) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--l1-background) !important;
opacity: 0.5 !important;
}
.cm-function {
color: var(--primary-background) !important;
}
}
}
}
}
}
}
.query-aggregation-error-popover {
.ant-popover-inner {
background-color: var(--l1-background);
border: none;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
}
}
}
.query-aggregation-error-popover {
.ant-popover-inner {
background-color: var(--l1-border);

View File

@@ -121,3 +121,44 @@
}
}
}
.lightMode {
.qb-trace-operator {
&-arrow {
&::before {
background: repeating-linear-gradient(
to right,
var(--l3-background),
var(--l3-background) 4px,
transparent 4px,
transparent 8px
);
}
&::after {
background-color: var(--l3-background);
}
}
&.non-list-view {
&::before {
background: repeating-linear-gradient(
to bottom,
var(--l3-background),
var(--l3-background) 4px,
transparent 4px,
transparent 8px
);
}
}
&-label-with-input {
border: 1px solid var(--l1-border) !important;
background: var(--l1-background) !important;
.label {
color: var(--l1-foreground) !important;
border-right: 1px solid var(--l1-border) !important;
background: var(--l1-background) !important;
}
}
}
}

View File

@@ -152,7 +152,7 @@
width: 100%;
height: 100%;
background: radial-gradient(circle, var(--l1-foreground) 10%, transparent 0);
background: radial-gradient(circle, #fff 10%, transparent 0);
background-size: 12px 12px;
opacity: 1;

View File

@@ -12,12 +12,6 @@
flex-shrink: 0;
}
&__layout {
display: flex;
flex-direction: column;
height: 100%;
}
&__tab-group {
[data-slot='toggle-group'] {
border-radius: 2px;

View File

@@ -197,3 +197,17 @@
background-color: var(--l1-border);
}
}
.lightMode {
.sa-table {
.ant-table-tbody {
> tr.sa-table-row--tinted > td {
background: rgba(0, 0, 0, 0.015);
}
> tr:hover > td {
background: rgba(0, 0, 0, 0.03) !important;
}
}
}
}

View File

@@ -147,12 +147,12 @@
left: 50%;
width: 4px;
transform: translateX(-50%);
background: var(--l2-background);
opacity: 1;
pointer-events: none;
transition:
background 120ms ease,
width 120ms ease;
background: transparent;
}
.cursorColResize:hover .tanstackResizeHandleLine {

View File

@@ -186,3 +186,29 @@
letter-spacing: -0.06px;
}
}
.lightMode {
.warning-content {
&__warning-code {
color: var(--l2-foreground);
}
&__warning-message {
color: var(--l1-foreground);
}
&__message-item {
color: var(--l1-foreground);
}
&__message-badge {
&-label-text {
color: var(--l1-foreground);
}
.key-value-label__value {
color: var(--l1-foreground);
}
}
&__docs-button {
background: var(--l1-background);
color: var(--l2-foreground);
}
}
}

View File

@@ -9,5 +9,4 @@ export enum FeatureKeys {
ANOMALY_DETECTION = 'anomaly_detection',
ONBOARDING_V3 = 'onboarding_v3',
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
BODY_JSON_ENABLED = 'body_json_enabled',
}

View File

@@ -87,7 +87,6 @@ const ROUTES = {
HOME_PAGE: '/',
PUBLIC_DASHBOARD: '/public/dashboard/:dashboardId',
SERVICE_ACCOUNTS_SETTINGS: '/settings/service-accounts',
MCP_SERVER: '/settings/mcp-server',
} as const;
export default ROUTES;

View File

@@ -157,3 +157,9 @@
.view-all-drawer {
border-radius: 4px;
}
.lightMode {
.ant-table {
background: inherit;
}
}

View File

@@ -226,3 +226,54 @@
}
}
}
.lightMode {
.api-quick-filter-left-section {
.api-quick-filters-header {
border-bottom: 1px solid var(--bg-vanilla-300);
}
}
.api-module-right-section {
.toolbar {
border-bottom: 1px solid var(--bg-vanilla-300);
}
}
.no-filtered-domains-message-container {
.no-filtered-domains-message-content {
.no-filtered-domains-message {
.no-domain-title {
color: var(--l1-foreground);
}
.no-domain-subtitle {
color: var(--l2-foreground);
.attribute {
font-family: 'Space Mono';
}
}
}
}
}
.api-monitoring-domain-list-table {
.ant-table {
.ant-table-cell {
color: var(--l1-foreground);
}
.table-row-light {
background: none;
}
.table-row-dark {
background: none;
}
.round-metric-tag {
color: var(--l1-foreground);
}
}
}
}

View File

@@ -28,7 +28,7 @@
}
.dashboard-title {
color: var(--l1-foreground);
color: #fff;
font-family: Inter;
font-size: 16px;
font-style: normal;
@@ -463,3 +463,135 @@
}
}
}
.lightMode {
.dashboard-description-container {
color: var(--l1-foreground);
.dashboard-details {
.left-section {
.dashboard-title {
color: var(--l1-foreground);
}
}
.right-section {
.icons {
border: 1px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
}
.configure-button {
border: 1px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
}
}
}
.dashboard-description-section {
color: var(--l1-foreground);
}
}
.dashboard-settings {
.ant-popover-inner {
border: 1px solid var(--l1-border);
background: var(--l1-background) !important;
}
.menu-content {
display: flex;
flex-direction: column;
.section-1 {
border-bottom: 1px solid var(--l1-border);
.ant-btn {
color: var(--l1-foreground);
}
}
.section-2 {
border-bottom: 1px solid var(--l1-border);
.ant-btn {
color: var(--l1-foreground);
}
}
}
}
.rename-dashboard {
.ant-modal-content {
border: 1px solid var(--l1-border);
background: var(--l1-background);
.ant-modal-header {
background: var(--l1-background);
border-bottom: 1px solid var(--l1-border);
.ant-modal-title {
color: var(--l1-foreground);
}
}
.ant-modal-body {
.dashboard-content {
.name-text {
color: var(--l1-foreground);
}
.dashboard-name-input {
border: 1px solid var(--l1-border);
background: var(--l1-background);
}
}
}
.ant-modal-footer {
.dashboard-rename {
.cancel-btn {
background: var(--l3-background);
}
}
}
}
}
.section-naming {
.ant-modal-content {
border: 1px solid var(--l1-border);
background: var(--l1-background);
.ant-modal-header {
background: var(--l1-background);
border-bottom: 1px solid var(--l1-border);
.ant-modal-title {
color: var(--l1-foreground);
}
}
.ant-modal-body {
.section-naming-content {
.name-text {
color: var(--l1-foreground);
}
.section-name-input {
border: 1px solid var(--l1-border);
background: var(--l1-background);
}
}
}
.ant-modal-footer {
.dashboard-rename {
.cancel-btn {
background: var(--l3-background);
}
}
}
}
}
}

View File

@@ -141,3 +141,58 @@
}
}
}
.lightMode {
.overview-content {
.overview-settings {
border: 1px solid var(--l1-border);
.name-icon-input {
.dashboard-image-input {
.ant-select-selector {
border: 1px solid var(--l1-border);
background: var(--l3-background);
}
}
.dashboard-name-input {
border: 1px solid var(--l1-border);
background: var(--l3-background);
}
}
.dashboard-name {
color: var(--l1-foreground);
}
.description-text-area {
border: 1px solid var(--l1-border);
background: var(--l3-background);
}
}
.overview-settings-footer {
border-top: 1px solid var(--l1-border);
background: var(--l1-background);
.unsaved {
.unsaved-dot {
background: var(--primary-background);
}
.unsaved-changes {
color: var(--bg-robin-400);
}
}
.footer-action-btns {
.discard-btn {
color: var(--l1-foreground);
background-color: var(--l3-background);
}
.save-btn {
color: var(--l3-background);
}
}
}
}
}

View File

@@ -100,6 +100,27 @@
max-width: 500px;
}
.lightMode {
.variable-item {
.variable-name {
border: 1px solid var(--l1-border);
background: var(--l1-background);
color: var(--bg-robin-300);
}
.variable-value {
border: 1px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
&:hover,
&:focus-within {
outline: 1px solid var(--bg-robin-400);
}
}
}
}
.cycle-error-alert {
margin-bottom: 12px;
padding: 4px 12px;

View File

@@ -116,3 +116,50 @@
}
}
}
.lightMode {
.panel-type-selection-modal {
.ant-modal-content {
border: 1px solid var(--l1-border);
background: var(--l1-background);
.ant-modal-header {
background: var(--l1-background);
border-bottom: 1px solid var(--l1-border);
.ant-modal-title {
color: var(--l1-foreground);
}
}
.ant-modal-body {
.panel-selection {
.selected {
background: var(--l2-background);
}
.ant-card {
border: 1px solid var(--l1-border);
.ant-card-body {
.ant-typography {
color: var(--l2-foreground);
}
}
}
}
}
.ant-modal-footer {
border-top: 1px solid var(--l1-border);
background: var(--l1-background);
.ant-btn {
color: var(--l1-foreground);
background: var(--primary-background);
}
}
}
}
}

View File

@@ -61,3 +61,11 @@
width: 14px;
}
}
.lightMode {
.dashboard-breadcrumbs {
.dashboard-btn {
color: var(--l1-foreground);
}
}
}

View File

@@ -57,3 +57,32 @@
}
}
}
.lightMode {
.download-logs-popover {
.ant-popover-inner {
border: 1px solid var(--l1-border);
background: linear-gradient(
139deg,
color-mix(in srgb, var(--card) 80%, transparent) 0%,
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
.download-logs-content {
.action-btns {
color: var(--l1-foreground);
}
.action-btns:hover {
&.ant-btn-text {
background-color: var(--l3-background) !important;
}
}
.export-heading {
color: var(--l2-foreground);
}
}
}
}
}

View File

@@ -139,8 +139,8 @@ function ChartPreview({
if (startTime && endTime && startTime !== endTime) {
dispatch(
UpdateTimeInterval('custom', [
Number.parseInt(getTimeString(startTime), 10),
Number.parseInt(getTimeString(endTime), 10),
parseInt(getTimeString(startTime), 10),
parseInt(getTimeString(endTime), 10),
]),
);
}

View File

@@ -119,6 +119,41 @@
}
}
.lightMode {
.main-container {
.plot-tag {
background: var(--l3-background);
}
}
.ant-modal-content {
background-color: var(--l1-foreground);
.ant-modal-confirm-title {
color: var(--l1-foreground);
}
.ant-modal-confirm-content {
.ant-typography {
color: var(--l1-foreground);
}
}
.ant-modal-confirm-btns {
button:nth-of-type(1) {
background-color: var(--l3-background);
border: none;
color: var(--l1-foreground);
}
}
}
.info-help-btns {
.doc-redirection-btn {
color: var(--bg-aqua-600) !important;
border-color: var(--bg-aqua-600) !important;
}
}
}
.create-notification-btn {
box-shadow: none;
}

View File

@@ -370,7 +370,7 @@ function FormAlertRules({
// onQueryCategoryChange handles changes to query category
// in state as well as sets additional defaults
const onQueryCategoryChange = (val: EQueryType): void => {
const element = document.querySelector('#top');
const element = document.getElementById('top');
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}

View File

@@ -0,0 +1,50 @@
// eslint-disable-next-line no-restricted-imports
import { createMachine } from 'xstate';
export const ResourceAttributesFilterMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QBECGsAWAjA9qgThAAQDKYBAxhkQIIB2xAYgJYA2ALmPgHQAqqUANJgAngGIAcgFEAGr0SgADjljN2zHHQUgAHogAcAFgAM3AOz6ATAEYAzJdsA2Y4cOWAnABoQIxAFpDR2tuQ319AFYTcKdbFycAX3jvNExcAmIySmp6JjZOHn4hUTFNACFWAFd8bWVVdU1tPQQzY1MXY2tDdzNHM3dHd0NvXwR7biMTa313S0i+63DE5PRsPEJScnwqWgYiFg4uPgFhcQAlKRIpeSQQWrUNLRumx3Czbg8TR0sbS31jfUcw38fW47gBHmm4XCVms3SWIBSq3SGyyO1yBx4AHlFFxUOwcPhJLJrkoVPcGk9ENYFuF3i5YR0wtEHECEAEgiEmV8zH1DLYzHZ4Yi0utMltsrt9vluNjcfjCWVKtUbnd6o9QE1rMYBtxbGFvsZ3NrZj1WdYOfotUZLX0XEFHEKViKMpttjk9nlDrL8HiCWJzpcSbcyWrGoh3NCQj0zK53P1ph1WeFLLqnJZ2s5vmZLA6kginWsXaj3VLDoUAGqoSpgEp0cpVGohh5hhDWDy0sz8zruakzamWVm-Qyg362V5-AZOayO1KFlHitEejFHKCV6v+i5XRt1ZuU1s52zjNOOaZfdOWIY+RDZ0Hc6ZmKEXqyLPPCudit2Sz08ACSEFYNbSHI27kuquiIOEjiONwjJgrM3RWJYZisgEIJgnYPTmuEdi2OaiR5nQOAQHA2hvsiH4Sui0qFCcIGhnuLSmP0YJuJ2xjJsmKELG8XZTK0tjdHG06vgW5GupRS7St6vrKqSO4UhqVL8TBWp8o4eqdl0A5Xmy3G6gK56-B4uERDOSKiuJi6lgUAhrhUYB0buimtrEKZBDYrxaS0OZca8+ltheybOI4hivGZzrzp+VGHH+AGOQp4EIHy+ghNYnawtG4TsbYvk8QKfHGAJfQ9uF76WSW37xWBTSGJ0qXpd0vRZdEKGPqC2YeO2-zfO4+HxEAA */
createMachine({
tsTypes: {} as import('./Labels.machine.typegen').Typegen0,
initial: 'Idle',
states: {
LabelKey: {
on: {
NEXT: {
actions: 'onSelectLabelValue',
target: 'LabelValue',
},
onBlur: {
actions: 'onSelectLabelValue',
target: 'LabelValue',
},
RESET: {
target: 'Idle',
},
},
},
LabelValue: {
on: {
NEXT: {
actions: ['onValidateQuery'],
},
onBlur: {
actions: ['onValidateQuery'],
// target: 'Idle',
},
RESET: {
target: 'Idle',
},
},
},
Idle: {
on: {
NEXT: {
actions: 'onSelectLabelKey',
description: 'Enter a label key',
target: 'LabelKey',
},
},
},
},
id: 'Label Key Values',
});

View File

@@ -0,0 +1,25 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true;
eventsCausingActions: {
onSelectLabelValue: 'NEXT' | 'onBlur';
onValidateQuery: 'NEXT' | 'onBlur';
onSelectLabelKey: 'NEXT';
};
internalEvents: {
'xstate.init': { type: 'xstate.init' };
};
invokeSrcNameMap: {};
missingImplementations: {
actions: 'onSelectLabelValue' | 'onValidateQuery' | 'onSelectLabelKey';
services: never;
guards: never;
delays: never;
};
eventsCausingServices: {};
eventsCausingGuards: {};
eventsCausingDelays: {};
matchesStates: 'LabelKey' | 'LabelValue' | 'Idle';
tags: never;
}

View File

@@ -4,20 +4,20 @@ import {
CloseCircleFilled,
ExclamationCircleOutlined,
} from '@ant-design/icons';
// eslint-disable-next-line no-restricted-imports
import { useMachine } from '@xstate/react';
import { Button, Input, message, Modal } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { map } from 'lodash-es';
import { Labels } from 'types/api/alerts/def';
import { v4 as uuid } from 'uuid';
import { ResourceAttributesFilterMachine } from './Labels.machine';
import QueryChip from './QueryChip';
import { QueryChipItem, SearchContainer } from './styles';
import { ILabelRecord } from './types';
import { createQuery, flattenLabels, prepareLabels } from './utils';
type LabelStep = 'Idle' | 'LabelKey' | 'LabelValue';
type LabelEvent = 'NEXT' | 'onBlur' | 'RESET';
interface LabelSelectProps {
onSetLabels: (q: Labels) => void;
initialValues: Labels | undefined;
@@ -35,65 +35,42 @@ function LabelSelect({
const [queries, setQueries] = useState<ILabelRecord[]>(
initialValues ? flattenLabels(initialValues) : [],
);
const [step, setStep] = useState<LabelStep>('Idle');
const dispatchChanges = (updatedRecs: ILabelRecord[]): void => {
onSetLabels(prepareLabels(updatedRecs, initialValues));
setQueries(updatedRecs);
};
const onSelectLabelValue = (): void => {
if (currentVal !== '') {
setStaging((prevState) => [...prevState, currentVal]);
} else {
return;
}
setCurrentVal('');
};
const [state, send] = useMachine(ResourceAttributesFilterMachine, {
actions: {
onSelectLabelKey: () => {},
onSelectLabelValue: () => {
if (currentVal !== '') {
setStaging((prevState) => [...prevState, currentVal]);
} else {
return;
}
setCurrentVal('');
},
onValidateQuery: (): void => {
if (currentVal === '') {
return;
}
const onValidateQuery = (): void => {
if (currentVal === '') {
return;
}
const generatedQuery = createQuery([...staging, currentVal]);
const generatedQuery = createQuery([...staging, currentVal]);
if (generatedQuery) {
dispatchChanges([...queries, generatedQuery]);
setStaging([]);
setCurrentVal('');
setStep('Idle');
}
};
const send = (event: LabelEvent): void => {
if (event === 'RESET') {
setStep('Idle');
return;
}
if (event === 'NEXT') {
if (step === 'Idle') {
setStep('LabelKey');
} else if (step === 'LabelKey') {
onSelectLabelValue();
setStep('LabelValue');
} else if (step === 'LabelValue') {
onValidateQuery();
}
return;
}
if (event === 'onBlur') {
if (step === 'LabelKey') {
onSelectLabelValue();
setStep('LabelValue');
} else if (step === 'LabelValue') {
onValidateQuery();
}
}
};
if (generatedQuery) {
dispatchChanges([...queries, generatedQuery]);
setStaging([]);
setCurrentVal('');
send('RESET');
}
},
},
});
const handleFocus = (): void => {
if (step === 'Idle') {
if (state.value === 'Idle') {
send('NEXT');
}
};
@@ -102,7 +79,7 @@ function LabelSelect({
if (staging.length === 1 && staging[0] !== undefined) {
send('onBlur');
}
}, [staging]);
}, [send, staging]);
useEffect(() => {
handleBlur();
@@ -138,14 +115,14 @@ function LabelSelect({
});
};
const renderPlaceholder = useCallback((): string => {
if (step === 'LabelKey') {
if (state.value === 'LabelKey') {
return 'Enter a label key then press ENTER.';
}
if (step === 'LabelValue') {
if (state.value === 'LabelValue') {
return `Enter a value for label key(${staging[0]}) then press ENTER.`;
}
return t('placeholder_label_key_pair');
}, [t, step, staging]);
}, [t, state, staging]);
return (
<SearchContainer isDarkMode={isDarkMode} disabled={false}>
<div style={{ display: 'inline-flex', flexWrap: 'wrap' }}>
@@ -171,7 +148,7 @@ function LabelSelect({
if (e.key === 'Enter' || e.code === 'Enter' || e.key === ':') {
send('NEXT');
}
if (step === 'Idle') {
if (state.value === 'Idle') {
send('NEXT');
}
}}
@@ -182,7 +159,7 @@ function LabelSelect({
onBlur={handleBlur}
/>
{queries.length > 0 || staging.length > 0 || currentVal ? (
{queries.length || staging.length || currentVal ? (
<Button
onClick={handleClearAll}
icon={<CloseCircleFilled />}

View File

@@ -174,3 +174,23 @@
}
}
}
.lightMode {
.dashboard-empty-state {
.dashboard-content {
.heading {
.icons {
color: var(--l1-foreground);
}
.welcome {
color: var(--l1-foreground);
}
.welcome-info {
color: var(--l1-foreground);
}
}
}
}
}

View File

@@ -333,3 +333,100 @@
}
}
}
.lightMode {
.fullscreen-grid-container {
.react-grid-layout {
.row-panel {
background: var(--l2-background);
.settings-icon {
color: var(--l1-foreground);
}
.row-icon {
color: var(--l1-foreground);
}
.section-title {
color: var(--l1-foreground);
}
}
}
}
.widget-full-view {
.ant-modal-content {
background-color: var(--l1-foreground);
.ant-modal-header {
background-color: var(--l1-foreground);
}
}
}
.row-settings {
.ant-popover-inner {
border: 1px solid var(--l1-border);
background: var(--l1-background);
.menu-content {
.section-1 {
.rename-btn {
color: var(--l1-foreground);
}
}
.section-2 {
border-top: 1px solid var(--l1-border);
.remove-section {
color: var(--bg-cherry-400);
}
}
}
}
}
.rename-section {
.ant-modal-content {
border: 1px solid var(--l1-border);
background: var(--l1-background);
.ant-modal-header {
background: var(--l1-background);
border-bottom: 1px solid var(--l1-border);
.ant-modal-title {
color: var(--l1-foreground);
}
}
.ant-modal-body {
.typography {
color: var(--l2-foreground);
}
.ant-form-item {
.action-btns {
.cancel-btn {
color: var(--l1-foreground);
background: var(--l3-background);
}
}
}
}
}
}
.view-onclick-show-button {
background: var(--l2-background);
border-color: var(--l1-foreground);
color: var(--l1-foreground);
.menu-item {
&:hover {
background-color: var(--l2-foreground);
}
}
}
}

View File

@@ -55,6 +55,14 @@
padding-right: 0.25rem;
}
.lightMode {
.widget-header-container {
.ant-input-group-addon {
background-color: inherit;
}
}
}
.long-tooltip {
.ant-tooltip-content {
max-height: 500px;

View File

@@ -482,10 +482,10 @@
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
&.selected {
background: var(--l3-background);
color: var(--primary);
background: var(--l2-background);
}
}
}

View File

@@ -21,9 +21,9 @@ import { DataSource } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import floppyDiscUrl from '@/assets/Icons/floppy-disc.svg';
import logsUrl from '@/assets/Icons/logs.svg';
import { getItemIcon } from '../constants';
import { ScrollText } from '@signozhq/icons';
export default function SavedViews({
onUpdateChecklistDoneItem,
@@ -351,7 +351,7 @@ export default function SavedViews({
className={selectedEntity === 'logs' ? 'selected tab' : 'tab'}
onClick={(): void => handleTabChange('logs')}
>
<ScrollText size={14} />
<img src={logsUrl} alt="logs-icon" className="logs-icon" />
Logs
</Button>
<Button

View File

@@ -148,3 +148,50 @@
}
}
}
.lightMode {
.entity-metric-traces-header {
.filter-section {
border-top: 1px solid var(--l1-border);
border-bottom: 1px solid var(--l1-border);
.ant-select-selector {
border-color: var(--l1-border) !important;
color: var(--l2-foreground);
}
}
}
.entity-metric-traces-table {
.ant-table {
border-radius: 3px;
border: 1px solid var(--l1-border);
.ant-table-thead > tr > th {
background: var(--l1-foreground);
color: var(--l2-foreground);
}
.ant-table-thead > tr > th:has(.entityname-column-header) {
background: var(--l1-foreground);
}
.ant-table-cell {
background: var(--l1-foreground);
color: var(--l1-foreground);
}
.ant-table-cell:has(.entityname-column-value) {
background: var(--l1-foreground);
}
.entityname-column-value {
color: var(--l1-foreground);
}
.ant-table-tbody > tr:hover > td {
background: rgba(0, 0, 0, 0.04);
}
}
}
}

View File

@@ -192,6 +192,63 @@
}
}
.lightMode {
.ant-drawer-header {
border-bottom: 1px solid var(--l1-border);
background: var(--l1-foreground);
}
.entity-detail-drawer {
.title {
color: var(--l2-foreground);
}
.entity-detail-drawer__entity {
.ant-typography {
color: var(--l2-foreground);
background: transparent;
}
}
.radio-button {
border: 1px solid var(--l1-border);
background: var(--l1-foreground);
color: var(--l2-foreground);
}
.views-tabs {
.tab {
background: var(--l1-foreground);
}
.selected_view {
background: var(--l3-background);
border: 1px solid var(--l1-border);
color: var(--l2-foreground);
}
.selected_view::before {
background: var(--l3-background);
border-left: 1px solid var(--l1-border);
}
}
.compass-button {
border: 1px solid var(--l1-border);
background: var(--l1-foreground);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
.tabs-and-search {
.action-btn {
border: 1px solid var(--l1-border);
background: var(--l1-foreground);
color: var(--l2-foreground);
}
}
}
}
.entity-metric-traces {
margin-top: 1rem;
@@ -342,6 +399,53 @@
}
}
.lightMode {
.entity-metric-traces-header {
.filter-section {
border-top: 1px solid var(--l1-border);
border-bottom: 1px solid var(--l1-border);
.ant-select-selector {
border-color: var(--l1-border) !important;
color: var(--l2-foreground);
}
}
}
.entity-metric-traces-table {
.ant-table {
border-radius: 3px;
border: 1px solid var(--l1-border);
.ant-table-thead > tr > th {
background: var(--l1-foreground);
color: var(--l2-foreground);
}
.ant-table-thead > tr > th:has(.entityname-column-header) {
background: var(--l1-foreground);
}
.ant-table-cell {
background: var(--l1-foreground);
color: var(--l1-foreground);
}
.ant-table-cell:has(.entityname-column-value) {
background: var(--l1-foreground);
}
.entityname-column-value {
color: var(--l3-background);
}
.ant-table-tbody > tr:hover > td {
background: rgba(0, 0, 0, 0.04);
}
}
}
}
.entity-metrics-logs-container {
margin-top: 1rem;

View File

@@ -773,6 +773,206 @@
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
}
.lightMode {
.ingestion-key-container {
.ingestion-key-content {
.title {
color: var(--l1-foreground);
}
.ant-table-row {
.ant-table-cell {
background: var(--l1-background);
}
&:hover {
.ant-table-cell {
background: var(--l1-background) !important;
}
}
.column-render {
border: 1px solid var(--l1-border);
background: var(--l1-background);
.ant-collapse {
border: none;
.ant-collapse-header {
background: var(--l1-background);
}
.ant-collapse-content {
border-top: 1px solid var(--l1-border);
}
}
.title-with-action {
.ingestion-key-title {
.ant-typography {
color: var(--l1-foreground);
}
}
.ingestion-key-value {
background: var(--l3-background);
.ant-typography {
color: var(--l2-foreground);
}
.copy-key-btn {
cursor: pointer;
}
}
.action-btn {
.ant-typography {
color: var(--l2-foreground);
}
}
}
.ingestion-key-details {
border-top: 1px solid var(--l1-border);
.ingestion-key-tag {
background: var(--l3-background);
.tag-text {
color: var(--l1-foreground);
}
}
.ingestion-key-created-by {
color: var(--l2-foreground);
}
.ingestion-key-last-used-at {
.ant-typography {
color: var(--l2-foreground);
}
}
}
}
}
}
}
.delete-ingestion-key-modal {
.ant-modal-content {
border: 1px solid var(--l1-border);
background: var(--l1-background);
.ant-modal-header {
background: var(--l1-background);
.title {
color: var(--l1-foreground);
}
}
.ant-modal-body {
.ant-typography {
color: var(--l2-foreground);
}
.ingestion-key-input {
.ant-input {
background: var(--l3-background);
color: var(--l1-foreground);
}
}
}
.ant-modal-footer {
.cancel-btn {
background: var(--l3-background);
color: var(--l1-foreground);
}
}
}
}
.ingestion-key-info-container {
.user-email {
background: var(--l3-background);
}
.limits-data {
border: 1px solid var(--l1-border);
}
}
.ingestion-key-modal {
.ant-modal-content {
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
padding: 0;
.ant-modal-header {
background: none;
border-bottom: 1px solid var(--l1-border);
padding: 16px;
}
}
}
.ingestion-key-access-role {
.ant-radio-button-wrapper {
&.ant-radio-button-wrapper-checked {
color: var(--l1-foreground);
background: var(--l3-background);
border-color: var(--l1-border);
&:hover {
color: var(--l1-foreground);
background: var(--l3-background);
border-color: var(--l1-border);
&::before {
background-color: var(--l3-background);
}
}
&:focus {
color: var(--l1-foreground);
background: var(--l3-background);
border-color: var(--l1-border);
}
}
}
.tab {
border: 1px solid var(--l1-border);
&::before {
background: var(--l3-background);
}
&.selected {
background: var(--l3-background);
}
}
}
.copyable-text {
background: var(--l3-background);
}
.ingestion-key-expires-at {
border: 1px solid var(--l1-border);
background: var(--l1-background);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
}
.expires-at .ant-picker {
border-color: var(--l1-border) !important;
}
}
.mt-8 {
margin-top: 8px;
}
@@ -885,3 +1085,26 @@
color: var(--l2-foreground);
}
}
.lightMode {
.ingestion-setup-details-links {
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
color: var(--accent-primary);
.learn-more {
color: var(--accent-primary);
}
}
.ingestion-url-error-tooltip {
.ingestion-url-error-content {
.ingestion-url-error-code {
color: var(--bg-amber-500);
}
}
.ingestion-url-error-message {
color: var(--l1-foreground);
}
}
}

View File

@@ -48,8 +48,7 @@ import { initialQueryMeterWithType } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { INITIAL_ALERT_THRESHOLD_STATE } from 'container/CreateAlertV2/context/constants';
import dayjs from 'dayjs';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useGetGlobalConfig } from 'api/generated/services/global';
import { useGetGlobalConfig } from 'hooks/globalConfig/useGetGlobalConfig';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useNotifications } from 'hooks/useNotifications';
import { cloneDeep, isNil, isUndefined } from 'lodash-es';
@@ -359,8 +358,6 @@ function MultiIngestionSettings(): JSX.Element {
error: globalConfigError,
} = useGetGlobalConfig();
const globalConfigApiError = convertToApiError(globalConfigError);
const { mutate: createIngestionKey, isLoading: isLoadingCreateAPIKey } =
useCreateIngestionKey<AxiosError<RenderErrorResponseDTO>>();
@@ -969,7 +966,7 @@ function MultiIngestionSettings(): JSX.Element {
<div className="ingestion-key-value">
<Typography.Text>
{APIKey?.value?.slice(0, 2)}********
{APIKey?.value?.substring(0, 2)}********
{APIKey?.value
?.substring(APIKey?.value?.length ? APIKey.value.length - 2 : 0)
?.trim()}
@@ -1607,11 +1604,11 @@ function MultiIngestionSettings(): JSX.Element {
title={
<div className="ingestion-url-error-content">
<Typography.Text className="ingestion-url-error-code">
{globalConfigApiError?.getErrorCode()}
{globalConfigError?.getErrorCode()}
</Typography.Text>
<Typography.Text className="ingestion-url-error-message">
{globalConfigApiError?.getErrorMessage()}
{globalConfigError?.getErrorMessage()}
</Typography.Text>
</div>
}

View File

@@ -9,8 +9,6 @@
.licenses-page-header-title {
color: var(--l1-foreground);
background: var(--l1-background);
border-right: 1px solid var(--l1-border);
text-align: center;
font-family: Inter;
font-size: 13px;
@@ -56,3 +54,38 @@
}
}
}
.lightMode {
.licenses-page {
.licenses-page-header {
border-bottom: 1px solid var(--l1-border);
background: var(--card);
backdrop-filter: blur(20px);
.licenses-page-header-title {
color: var(--l1-foreground);
background: var(--l1-background);
border-right: 1px solid var(--l1-border);
}
}
.licenses-page-content-container {
.licenses-page-content {
background: var(--l1-background);
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--l3-background);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--l3-background);
}
}
}
}
}

View File

@@ -176,3 +176,15 @@
cursor: pointer;
}
}
.lightMode {
.info-text {
color: var(--bg-robin-600) !important;
}
.info-link-container {
.anticon {
color: var(--bg-robin-400);
}
}
}

View File

@@ -1114,6 +1114,305 @@
}
}
.lightMode {
.dashboards-list-container {
.dashboards-list-view-content {
.title {
color: var(--l1-foreground);
}
.subtitle {
color: var(--muted-foreground);
}
.ant-table-row {
.ant-table-cell {
background: var(--l1-background);
}
&:hover {
.ant-table-cell {
background: var(--l1-background) !important;
}
}
.dashboard-list-item {
border: 1px solid var(--l1-border);
background: var(--card);
.dashboard-title {
color: var(--l2-foreground);
.title {
color: var(--l1-foreground);
}
}
.title-with-action {
.dashboard-title {
.ant-typography {
color: var(--l1-foreground);
}
}
.action-btn {
.ant-typography {
color: var(--l1-foreground);
}
}
}
.dashboard-details {
.dashboard-tag {
background: var(--l3-background);
.tag-text {
color: var(--l1-foreground);
}
}
.created-by {
.dashboard-tag {
background: var(--l3-background);
.tag-text {
color: var(--l1-foreground);
}
}
.dashboard-created-by {
color: var(--l2-foreground);
}
}
.updated-by {
.text {
color: var(--l2-foreground);
}
.dashboard-tag {
background: var(--l3-background);
.tag-text {
color: var(--l1-foreground);
}
}
.dashboard-created-by {
color: var(--l2-foreground);
}
}
.dashboard-created-by {
color: var(--l1-foreground);
}
.dashboard-created-at {
.ant-typography {
color: var(--l1-foreground);
}
}
}
}
}
}
.no-search {
.text {
color: var(--muted-foreground);
}
}
.all-dashboards-header {
border: 1px solid var(--l1-border);
background: var(--l2-background);
.typography {
color: var(--l2-foreground);
}
.right-actions {
color: var(--l2-foreground);
}
}
.dashboard-empty-state {
.text {
.no-dashboard {
color: var(--l2-foreground);
}
.info {
color: var(--l2-foreground);
}
}
}
.dashboard-error-state {
.error-text {
color: var(--muted-foreground);
}
.action-btns {
.retry-btn {
background: var(--l3-background);
color: var(--l1-foreground);
}
}
}
}
.delete-view-modal {
.ant-modal-content {
border: 1px solid var(--l1-border);
background: var(--card);
.ant-modal-header {
background: var(--card);
.title {
color: var(--l1-foreground);
}
}
.ant-modal-body {
.ant-typography {
color: var(--l1-foreground);
}
.save-view-input {
.ant-input {
background: var(--l2-background);
color: var(--l1-foreground);
}
}
}
.ant-modal-footer {
.cancel-btn {
background: var(--l3-background);
color: var(--l2-foreground);
}
}
}
}
.dashboard-actions {
.ant-popover-inner {
border: 1px solid var(--l1-border);
background: var(--card);
.dashboard-action-content {
.section-1 {
.action-btn {
color: var(--l2-foreground);
}
}
.section-2 {
border-top: 1px solid var(--l1-border);
}
}
}
}
.sort-dashboards {
.ant-popover-inner {
border: 1px solid var(--l1-border);
background: var(--card);
.sort-content {
.sort-heading {
color: var(--l2-foreground);
}
.sort-btns {
color: var(--l2-foreground);
}
}
}
}
.configure-group {
.ant-popover-inner {
border: 1px solid var(--l1-border);
background: var(--card);
.configure-content {
.configure-btn {
color: var(--l2-foreground);
}
}
}
}
.configure-metadata-root {
.ant-modal-content {
border: 1px solid var(--l1-border);
background: var(--card);
.ant-modal-header {
background: var(--card);
border-bottom: 1px solid var(--l1-border);
}
.ant-modal-body {
.configure-content {
.configure-preview {
border: 0.915px solid var(--l1-border);
background: var(--l2-background);
.header {
.title {
color: var(--l2-foreground);
}
}
.details {
.createdAt {
.formatted-time {
color: var(--l2-foreground);
}
.user {
.user-tag {
color: var(--l2-foreground);
background-color: var(--l3-background);
}
.dashboard-created-by {
color: var(--l2-foreground);
}
}
}
.updatedAt {
.formatted-time {
color: var(--l2-foreground);
}
.user {
.user-tag {
color: var(--l2-foreground);
background-color: var(--l3-background);
}
.dashboard-created-by {
color: var(--l2-foreground);
}
}
}
}
}
.metadata-action {
.connection-line {
border: 1px dashed var(--l1-border);
}
}
}
}
.ant-modal-footer {
.save-changes {
border: 1px solid var(--l1-border);
background: var(--l2-background);
}
}
}
}
}
.title-toolip {
.ant-tooltip-content {
.ant-tooltip-inner {

View File

@@ -194,3 +194,76 @@
border-top: 1px solid var(--l1-border);
}
}
.lightMode {
.new-dashboard-templates-modal {
.ant-modal-content {
border: 1px solid var(--l1-border);
background: var(--l1-foreground);
}
.new-dashboard-templates-content-header {
border-bottom: 1px solid var(--l1-border);
}
.new-dashboard-templates-content {
.new-dashboard-templates-list {
border-right: 1px solid var(--l1-border);
.templates-list {
.template-list-item {
.template-name {
color: var(--l3-background);
}
&:hover {
background: color-mix(in srgb, var(--bg-robin-200) 8%, transparent);
}
&.active {
background: color-mix(in srgb, var(--bg-robin-200) 8%, transparent);
}
}
}
}
.new-dashboard-template-preview {
.template-preview-header {
.template-preview-title {
.template-preview-icon {
border: 1px solid var(--l1-border);
background: var(--l1-foreground);
}
.template-info {
.template-name {
color: var(--l3-background);
}
.template-description {
color: var(--l2-foreground);
}
}
}
.create-dashboard-btn {
.ant-btn {
box-shadow: none;
}
}
}
.template-preview-image {
img {
border: 1px solid var(--l1-border);
background: var(--l1-foreground);
}
}
}
}
.ant-modal-footer {
border-top: 1px solid var(--l1-border);
}
}
}

View File

@@ -0,0 +1,51 @@
// eslint-disable-next-line no-restricted-imports
import { createMachine } from 'xstate';
export const DashboardSearchAndFilter = createMachine({
tsTypes: {} as import('./Dashboard.machine.typegen').Typegen0,
initial: 'Idle',
states: {
Category: {
on: {
NEXT: {
actions: 'onSelectOperator',
target: 'Operator',
},
onBlur: {
actions: 'onBlurPurge',
target: 'Idle',
},
},
},
Operator: {
on: {
NEXT: {
actions: 'onSelectValue',
target: 'Value',
},
onBlur: {
actions: 'onBlurPurge',
target: 'Idle',
},
},
},
Value: {
on: {
onBlur: {
actions: ['onValidateQuery', 'onBlurPurge'],
target: 'Idle',
},
},
},
Idle: {
on: {
NEXT: {
actions: 'onSelectCategory',
description: 'Select Category',
target: 'Category',
},
},
},
},
id: 'Dashboard Search And Filter',
});

View File

@@ -0,0 +1,32 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true;
eventsCausingActions: {
onSelectOperator: 'NEXT';
onBlurPurge: 'onBlur';
onSelectValue: 'NEXT';
onValidateQuery: 'onBlur';
onSelectCategory: 'NEXT';
};
internalEvents: {
'xstate.init': { type: 'xstate.init' };
};
invokeSrcNameMap: {};
missingImplementations: {
actions:
| 'onSelectOperator'
| 'onBlurPurge'
| 'onSelectValue'
| 'onValidateQuery'
| 'onSelectCategory';
services: never;
guards: never;
delays: never;
};
eventsCausingServices: {};
eventsCausingGuards: {};
eventsCausingDelays: {};
matchesStates: 'Category' | 'Operator' | 'Value' | 'Idle';
tags: never;
}

View File

@@ -0,0 +1,21 @@
import { QueryChipContainer, QueryChipItem } from './styles';
import { IQueryStructure } from './types';
export default function QueryChip({
queryData,
onRemove,
}: {
queryData: IQueryStructure;
onRemove: (id: string) => void;
}): JSX.Element {
const { category, operator, value, id } = queryData;
return (
<QueryChipContainer>
<QueryChipItem>{category}</QueryChipItem>
<QueryChipItem>{operator}</QueryChipItem>
<QueryChipItem closable onClose={(): void => onRemove(id)}>
{Array.isArray(value) ? value.join(', ') : null}
</QueryChipItem>
</QueryChipContainer>
);
}

View File

@@ -0,0 +1,64 @@
import { Dashboard } from 'types/api/dashboard/getAll';
import { v4 as uuid } from 'uuid';
import { TOperator } from '../types';
import { executeSearchQueries } from '../utils';
describe('executeSearchQueries', () => {
const firstDashboard: Dashboard = {
id: uuid(),
createdAt: '',
updatedAt: '',
createdBy: '',
updatedBy: '',
data: {
title: 'first dashboard',
variables: {},
},
};
const secondDashboard: Dashboard = {
id: uuid(),
createdAt: '',
updatedAt: '',
createdBy: '',
updatedBy: '',
data: {
title: 'second dashboard',
variables: {},
},
};
const thirdDashboard: Dashboard = {
id: uuid(),
createdAt: '',
updatedAt: '',
createdBy: '',
updatedBy: '',
data: {
title: 'third dashboard (with special characters +?\\)',
variables: {},
},
};
const dashboards = [firstDashboard, secondDashboard, thirdDashboard];
it('should filter dashboards based on title', () => {
const query = {
category: 'title',
id: 'someid',
operator: '=' as TOperator,
value: 'first dashboard',
};
expect(executeSearchQueries([query], dashboards)).toEqual([firstDashboard]);
});
it('should filter dashboards with special characters', () => {
const query = {
category: 'title',
id: 'someid',
operator: '=' as TOperator,
value: 'third dashboard (with special characters +?\\)',
};
expect(executeSearchQueries([query], dashboards)).toEqual([thirdDashboard]);
});
});

View File

@@ -0,0 +1,212 @@
import {
MutableRefObject,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { CloseCircleFilled } from '@ant-design/icons';
// eslint-disable-next-line no-restricted-imports
import { useMachine } from '@xstate/react';
import { Button, RefSelectProps, Select } from 'antd';
import history from 'lib/history';
import { filter, map } from 'lodash-es';
import { Dashboard } from 'types/api/dashboard/getAll';
import { v4 as uuidv4 } from 'uuid';
import { DashboardSearchAndFilter } from './Dashboard.machine';
import QueryChip from './QueryChip';
import { QueryChipItem, SearchContainer } from './styles';
import { IOptionsData, IQueryStructure, TCategory, TOperator } from './types';
import {
convertQueriesToURLQuery,
convertURLQueryStringToQuery,
executeSearchQueries,
OptionsSchemas,
OptionsValueResolution,
} from './utils';
function SearchFilter({
searchData,
filterDashboards,
}: {
searchData: Dashboard[];
filterDashboards: (filteredDashboards: Dashboard[]) => void;
}): JSX.Element {
const [category, setCategory] = useState<TCategory>();
const [optionsData, setOptionsData] = useState<IOptionsData>(
OptionsSchemas.attribute,
);
const selectRef = useRef() as MutableRefObject<RefSelectProps>;
const [selectedValues, setSelectedValues] = useState<string[]>([]);
const [staging, setStaging] = useState<string[] | string[][] | unknown[]>([]);
const [queries, setQueries] = useState<IQueryStructure[]>([]);
useEffect(() => {
const searchQueryString = new URLSearchParams(history.location.search).get(
'search',
);
if (searchQueryString) {
setQueries(convertURLQueryStringToQuery(searchQueryString) || []);
}
}, []);
useEffect(() => {
filterDashboards(executeSearchQueries(queries, searchData));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queries, searchData]);
const updateURLWithQuery = useCallback(
(inputQueries?: IQueryStructure[]): void => {
history.push({
pathname: history.location.pathname,
search:
inputQueries || queries
? `?search=${convertQueriesToURLQuery(inputQueries || queries)}`
: '',
});
},
[queries],
);
useEffect(() => {
if (Array.isArray(queries) && queries.length > 0) {
updateURLWithQuery();
}
}, [queries, updateURLWithQuery]);
const [state, send] = useMachine(DashboardSearchAndFilter, {
actions: {
onSelectCategory: () => {
setOptionsData(OptionsSchemas.attribute);
},
onSelectOperator: () => {
setOptionsData(OptionsSchemas.operator);
},
onSelectValue: () => {
setOptionsData(
OptionsValueResolution(category as TCategory, searchData) as IOptionsData,
);
},
onBlurPurge: () => {
setSelectedValues([]);
setStaging([]);
},
onValidateQuery: () => {
if (staging.length <= 2 && selectedValues.length === 0) {
return;
}
setQueries([
...queries,
{
id: uuidv4(),
category: staging[0] as string,
operator: staging[1] as TOperator,
value: selectedValues,
},
]);
},
},
});
const nextState = (): void => {
send('NEXT');
};
const removeQueryById = (queryId: string): void => {
setQueries((queries) => {
const updatedQueries = filter(queries, ({ id }) => id !== queryId);
updateURLWithQuery(updatedQueries);
return updatedQueries;
});
};
const handleChange = (value: never | string[]): void => {
if (!value) {
return;
}
if (optionsData.mode) {
setSelectedValues(value.filter(Boolean));
return;
}
setStaging([...staging, value]);
if (state.value === 'Category') {
setCategory(`${value}`.toLowerCase() as TCategory);
}
nextState();
setSelectedValues([]);
};
const handleFocus = (): void => {
if (state.value === 'Idle') {
send('NEXT');
selectRef.current?.focus();
}
};
const handleBlur = (): void => {
send('onBlur');
selectRef?.current?.blur();
};
const clearQueries = (): void => {
setQueries([]);
history.push({
pathname: history.location.pathname,
search: ``,
});
};
return (
<SearchContainer>
<div>
{map(queries, (query) => (
<QueryChip key={query.id} queryData={query} onRemove={removeQueryById} />
))}
{map(staging, (value) => (
<QueryChipItem key={JSON.stringify(value)}>
{value as string}
</QueryChipItem>
))}
</div>
{optionsData && (
<Select
placeholder={
!queries.length &&
!staging.length &&
!selectedValues.length &&
'Search or Filter results'
}
size="small"
ref={selectRef}
mode={optionsData.mode as 'tags' | 'multiple'}
style={{ flex: 1 }}
onChange={handleChange}
bordered={false}
suffixIcon={null}
value={selectedValues}
onFocus={handleFocus}
onBlur={handleBlur}
showSearch
>
{optionsData.options &&
Array.isArray(optionsData.options) &&
optionsData.options.map(
(optionItem): JSX.Element => (
<Select.Option
key={(optionItem.value as string) || (optionItem.name as string)}
value={optionItem.value || optionItem.name}
>
{optionItem.name}
</Select.Option>
),
)}
</Select>
)}
{queries && queries.length > 0 && (
<Button icon={<CloseCircleFilled />} type="text" onClick={clearQueries} />
)}
</SearchContainer>
);
}
export default SearchFilter;

View File

@@ -0,0 +1,27 @@
import { grey } from '@ant-design/colors';
import { Tag } from 'antd';
import styled from 'styled-components';
export const SearchContainer = styled.div`
width: 100%;
display: flex;
align-items: center;
gap: 0.2rem;
padding: 0.2rem 0;
margin: 1rem 0;
border: 1px solid #ccc5;
`;
export const QueryChipContainer = styled.span`
display: flex;
align-items: center;
margin-right: 0.5rem;
&:hover {
& > * {
background: ${grey.primary}44;
}
}
`;
export const QueryChipItem = styled(Tag)`
margin-right: 0.1rem;
`;

View File

@@ -0,0 +1,18 @@
export type TOperator = '=' | '!=';
export type TCategory = 'title' | 'description' | 'tags';
export interface IQueryStructure {
category: string;
id: string;
operator: TOperator;
value: string | string[];
}
interface IOptions {
name: string;
value?: string;
}
export interface IOptionsData {
mode: undefined | 'tags' | 'multiple';
options: IOptions[] | [];
}

View File

@@ -0,0 +1,153 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { decode, encode } from 'js-base64';
import { flattenDeep, map, uniqWith } from 'lodash-es';
import { Dashboard } from 'types/api/dashboard/getAll';
import { IOptionsData, IQueryStructure, TCategory, TOperator } from './types';
export const convertQueriesToURLQuery = (
queries: IQueryStructure[],
): string => {
if (!queries || !queries.length) {
return '';
}
return encode(JSON.stringify(queries));
};
export const convertURLQueryStringToQuery = (
queryString: string,
): IQueryStructure[] => JSON.parse(decode(queryString));
export const resolveOperator = (
result: unknown,
operator: TOperator,
): boolean => {
if (operator === '!=') {
return !result;
}
if (operator === '=') {
return !!result;
}
return !!result;
};
export const executeSearchQueries = (
queries: IQueryStructure[] = [],
searchData: Dashboard[] = [],
): Dashboard[] => {
if (!searchData.length || !queries.length) {
return searchData;
}
const escapeRegExp = (regExp: string): string =>
regExp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
queries.forEach((query: IQueryStructure) => {
const { operator } = query;
let { value } = query;
const categoryLowercase: TCategory = `${query.category}`.toLowerCase() as
| 'title'
| 'description';
value = flattenDeep([value]);
searchData = searchData.filter(({ data: searchPayload }: Dashboard) => {
try {
const searchSpace =
flattenDeep([searchPayload[categoryLowercase]]).filter(Boolean) || null;
if (!searchSpace || !searchSpace.length) {
return resolveOperator(false, operator);
}
for (const searchSpaceItem of searchSpace) {
if (searchSpaceItem) {
for (const queryValue of value) {
if (searchSpaceItem.match(escapeRegExp(queryValue))) {
return resolveOperator(true, operator);
}
}
}
}
} catch (error) {
console.error(error);
}
return resolveOperator(false, operator);
});
});
return searchData;
};
export const OptionsSchemas = {
attribute: {
mode: undefined,
options: [
{
name: 'Title',
},
{
name: 'Description',
},
{
name: 'Tags',
},
],
},
operator: {
mode: undefined,
options: [
{
value: '=',
name: 'Equal',
},
{
name: 'Not Equal',
value: '!=',
},
],
},
};
export function OptionsValueResolution(
category: TCategory,
searchData: Dashboard[],
): Record<string, unknown> | IOptionsData {
const OptionsValueSchema = {
title: {
mode: 'tags',
options: uniqWith(
map(searchData, (searchItem) => ({ name: searchItem.data.title })),
(prev, next) => prev.name === next.name,
),
},
description: {
mode: 'tags',
options: uniqWith(
map(searchData, (searchItem) =>
searchItem.data.description
? {
name: searchItem.data.description,
value: searchItem.data.description,
}
: null,
).filter(Boolean),
(prev, next) => prev?.name === next?.name,
),
},
tags: {
mode: 'tags',
options: uniqWith(
map(
flattenDeep(
// @ts-ignore
map(searchData, (searchItem) => searchItem.data.tags).filter(Boolean),
),
(tag) => ({ name: tag }),
),
(prev, next) => prev.name === next.name,
),
},
};
return (
OptionsValueSchema[category] ||
({ mode: undefined, options: [] } as IOptionsData)
);
}

View File

@@ -7,3 +7,9 @@
.delete-btn:hover {
background-color: color-mix(in srgb, var(--l1-foreground) 12%, transparent);
}
.lightMode {
.delete-btn:hover {
background-color: rgba(0, 0, 0, 0.06) !important;
}
}

View File

@@ -8,19 +8,8 @@ import {
OPERATORS,
QUERY_BUILDER_FUNCTIONS,
} from 'constants/antlrQueryConstants';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { useAppContext } from 'providers/App/App';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TitleWrapper } from './BodyTitleRenderer.styles';
import { DROPDOWN_KEY } from './constant';
@@ -36,32 +25,17 @@ function BodyTitleRenderer({
parentIsArray = false,
nodeKey,
value,
handleChangeSelectedView,
}: BodyTitleRendererProps): JSX.Element {
const { onAddToQuery } = useActiveLog();
const { stagedQuery, updateQueriesData } = useQueryBuilder();
const { featureFlags } = useAppContext();
const [, setCopy] = useCopyToClipboard();
const { notifications } = useNotifications();
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
const cleanedNodeKey = removeObjectFromString(nodeKey);
const isBodyJsonQueryEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.BODY_JSON_ENABLED)
?.active || false;
// Group by is supported only for body json query enabled and not for array elements
const isGroupBySupported =
isBodyJsonQueryEnabled && !cleanedNodeKey.includes('[]');
const filterHandler = (isFilterIn: boolean) => (): void => {
if (parentIsArray) {
onAddToQuery(
generateFieldKeyForArray(
cleanedNodeKey,
removeObjectFromString(nodeKey),
getDataTypes(value),
isBodyJsonQueryEnabled,
),
`${value}`,
isFilterIn
@@ -71,7 +45,7 @@ function BodyTitleRenderer({
);
} else {
onAddToQuery(
`body.${cleanedNodeKey}`,
`body.${removeObjectFromString(nodeKey)}`,
`${value}`,
isFilterIn ? OPERATORS['='] : OPERATORS['!='],
getDataTypes(value),
@@ -79,67 +53,10 @@ function BodyTitleRenderer({
}
};
const groupByHandler = useCallback((): void => {
if (!stagedQuery) {
return;
}
const groupByKey = parentIsArray
? generateFieldKeyForArray(
cleanedNodeKey,
getDataTypes(value),
isBodyJsonQueryEnabled,
)
: `body.${cleanedNodeKey}`;
const fieldDataType = getDataTypes(value);
const normalizedDataType: DataTypes | undefined = Object.values(
DataTypes,
).includes(fieldDataType as DataTypes)
? (fieldDataType as DataTypes)
: undefined;
const updatedQuery = updateQueriesData(
stagedQuery,
'queryData',
(item, index) => {
if (index === 0) {
const newGroupByItem: BaseAutocompleteData = {
key: groupByKey,
type: '',
dataType: normalizedDataType,
};
return { ...item, groupBy: [...(item.groupBy || []), newGroupByItem] };
}
return item;
},
);
const queryData: ICurrentQueryData = {
name: viewName,
id: updatedQuery.id,
query: updatedQuery,
};
handleChangeSelectedView?.(ExplorerViews.TIMESERIES, queryData);
}, [
cleanedNodeKey,
handleChangeSelectedView,
isBodyJsonQueryEnabled,
parentIsArray,
stagedQuery,
updateQueriesData,
value,
viewName,
]);
const onClickHandler: MenuProps['onClick'] = (props): void => {
const mapper = {
[DROPDOWN_KEY.FILTER_IN]: filterHandler(true),
[DROPDOWN_KEY.FILTER_OUT]: filterHandler(false),
[DROPDOWN_KEY.GROUP_BY]: groupByHandler,
};
const handler = mapper[props.key];
@@ -159,14 +76,6 @@ function BodyTitleRenderer({
key: DROPDOWN_KEY.FILTER_OUT,
label: `Filter out ${value}`,
},
...(isGroupBySupported
? [
{
key: DROPDOWN_KEY.GROUP_BY,
label: `Group by ${nodeKey}`,
},
]
: []),
],
onClick: onClickHandler,
};
@@ -175,6 +84,7 @@ function BodyTitleRenderer({
(e: React.MouseEvent): void => {
// Prevent tree node expansion/collapse
e.stopPropagation();
const cleanedKey = removeObjectFromString(nodeKey);
let copyText: string;
// Check if value is an object or array
@@ -196,8 +106,8 @@ function BodyTitleRenderer({
if (copyText) {
const notificationMessage = isObject
? `${cleanedNodeKey} object copied to clipboard`
: `${cleanedNodeKey} copied to clipboard`;
? `${cleanedKey} object copied to clipboard`
: `${cleanedKey} copied to clipboard`;
notifications.success({
message: notificationMessage,
@@ -205,7 +115,7 @@ function BodyTitleRenderer({
});
}
},
[cleanedNodeKey, parentIsArray, setCopy, value, notifications],
[nodeKey, parentIsArray, setCopy, value, notifications],
);
return (

View File

@@ -1,4 +1,3 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { MetricsType } from 'container/MetricsApplication/constant';
import { ILog } from 'types/api/logs/log';
@@ -7,7 +6,6 @@ export interface BodyTitleRendererProps {
nodeKey: string;
value: unknown;
parentIsArray?: boolean;
handleChangeSelectedView?: ChangeViewFunctionType;
}
export type AnyObject = { [key: string]: any };

View File

@@ -58,3 +58,14 @@
}
}
}
.lightMode {
.table-view-actions-content {
.ant-popover-inner {
border: 1px solid var(--l1-border);
background: var(--l1-background) !important;
backdrop-filter: none;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
}
}

View File

@@ -2,7 +2,6 @@ import React, { useCallback, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Color } from '@signozhq/design-tokens';
import { Button, Popover, Spin, Tooltip, Tree } from 'antd';
import type { DataNode } from 'antd/es/tree';
import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
import cx from 'classnames';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
@@ -58,7 +57,7 @@ interface ITableViewActionsProps {
}
// Memoized Tree Component
const MemoizedTree = React.memo<{ treeData: DataNode[] }>(({ treeData }) => (
const MemoizedTree = React.memo<{ treeData: any[] }>(({ treeData }) => (
<Tree
defaultExpandAll
showLine
@@ -75,54 +74,50 @@ const BodyContent: React.FC<{
record: DataType;
bodyHtml: { __html: string };
textToCopy: string;
handleChangeSelectedView?: ChangeViewFunctionType;
}> = React.memo(
({ fieldData, record, bodyHtml, textToCopy, handleChangeSelectedView }) => {
const { isLoading, treeData, error } = useAsyncJSONProcessing(
fieldData.value,
record.field === 'body',
handleChangeSelectedView,
}> = React.memo(({ fieldData, record, bodyHtml, textToCopy }) => {
const { isLoading, treeData, error } = useAsyncJSONProcessing(
fieldData.value,
record.field === 'body',
);
// Show JSON tree if available, otherwise show HTML content
if (record.field === 'body' && treeData) {
return <MemoizedTree treeData={treeData} />;
}
if (record.field === 'body' && isLoading) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Spin size="small" />
<span style={{ color: Color.BG_SIENNA_400 }}>Processing JSON...</span>
</div>
);
}
// Show JSON tree if available, otherwise show HTML content
if (record.field === 'body' && treeData) {
return <MemoizedTree treeData={treeData} />;
}
if (record.field === 'body' && error) {
return (
<span
style={{ color: Color.BG_SIENNA_400, whiteSpace: 'pre-wrap', tabSize: 4 }}
>
Error parsing Body JSON
</span>
);
}
if (record.field === 'body' && isLoading) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Spin size="small" />
<span style={{ color: Color.BG_SIENNA_400 }}>Processing JSON...</span>
</div>
);
}
if (record.field === 'body' && error) {
return (
if (record.field === 'body') {
return (
<CopyClipboardHOC entityKey="body" textToCopy={textToCopy}>
<span
style={{ color: Color.BG_SIENNA_400, whiteSpace: 'pre-wrap', tabSize: 4 }}
>
Error parsing Body JSON
<span dangerouslySetInnerHTML={bodyHtml} />
</span>
);
}
</CopyClipboardHOC>
);
}
if (record.field === 'body') {
return (
<CopyClipboardHOC entityKey="body" textToCopy={textToCopy}>
<span
style={{ color: Color.BG_SIENNA_400, whiteSpace: 'pre-wrap', tabSize: 4 }}
>
<span dangerouslySetInnerHTML={bodyHtml} />
</span>
</CopyClipboardHOC>
);
}
return null;
},
);
return null;
});
BodyContent.displayName = 'BodyContent';
@@ -324,7 +319,6 @@ export default function TableViewActions(
record={record}
bodyHtml={bodyHtml}
textToCopy={textToCopy}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
@@ -348,7 +342,6 @@ export default function TableViewActions(
fieldData,
bodyHtml,
textToCopy,
handleChangeSelectedView,
formatTimezoneAdjustedTimestamp,
cleanTimestamp,
]);
@@ -362,7 +355,6 @@ export default function TableViewActions(
record={record}
bodyHtml={bodyHtml}
textToCopy={textToCopy}
handleChangeSelectedView={handleChangeSelectedView}
/>
{!isListViewPanel &&
!RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey) && (

View File

@@ -1,8 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { FeatureKeys } from 'constants/features';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { isEmpty } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { jsonToDataNodes, recursiveParseJSON } from '../utils';
@@ -12,7 +9,6 @@ const MAX_BODY_BYTES = 100 * 1024; // 100 KB
const useAsyncJSONProcessing = (
value: string,
shouldProcess: boolean,
handleChangeSelectedView?: ChangeViewFunctionType,
): {
isLoading: boolean;
treeData: any[] | null;
@@ -29,10 +25,6 @@ const useAsyncJSONProcessing = (
});
const processingRef = useRef<boolean>(false);
const { featureFlags } = useAppContext();
const isBodyJsonQueryEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.BODY_JSON_ENABLED)
?.active || false;
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect((): (() => void) => {
@@ -55,10 +47,7 @@ const useAsyncJSONProcessing = (
try {
const parsedBody = recursiveParseJSON(value);
if (!isEmpty(parsedBody)) {
const treeData = jsonToDataNodes(parsedBody, {
isBodyJsonQueryEnabled,
handleChangeSelectedView,
});
const treeData = jsonToDataNodes(parsedBody);
setJsonState({ isLoading: false, treeData, error: null });
} else {
setJsonState({ isLoading: false, treeData: null, error: null });
@@ -84,10 +73,7 @@ const useAsyncJSONProcessing = (
try {
const parsedBody = recursiveParseJSON(value);
if (!isEmpty(parsedBody)) {
const treeData = jsonToDataNodes(parsedBody, {
isBodyJsonQueryEnabled,
handleChangeSelectedView,
});
const treeData = jsonToDataNodes(parsedBody);
setJsonState({ isLoading: false, treeData, error: null });
} else {
setJsonState({ isLoading: false, treeData: null, error: null });
@@ -115,7 +101,7 @@ const useAsyncJSONProcessing = (
return (): void => {
processingRef.current = false;
};
}, [value, shouldProcess, isBodyJsonQueryEnabled, handleChangeSelectedView]);
}, [value, shouldProcess]);
return jsonState;
};

View File

@@ -1,5 +1,4 @@
export const DROPDOWN_KEY = {
FILTER_IN: 'filterIn',
FILTER_OUT: 'filterOut',
GROUP_BY: 'groupBy',
};

View File

@@ -1,6 +1,5 @@
import Convert from 'ansi-to-html';
import type { DataNode } from 'antd/es/tree';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { MetricsType } from 'container/MetricsApplication/constant';
import dompurify from 'dompurify';
import { uniqueId } from 'lodash-es';
@@ -35,32 +34,13 @@ export const recursiveParseJSON = (obj: string): Record<string, unknown> => {
}
};
type JsonToDataNodesOptions = {
parentKey?: string;
parentIsArray?: boolean;
isBodyJsonQueryEnabled?: boolean;
handleChangeSelectedView?: ChangeViewFunctionType;
};
type ComputeDataNodeOptions = {
key: string;
valueIsArray: boolean;
value: unknown;
nodeKey: string;
parentIsArray: boolean;
isBodyJsonQueryEnabled?: boolean;
handleChangeSelectedView?: ChangeViewFunctionType;
};
export const computeDataNode = ({
key,
valueIsArray,
value,
nodeKey,
parentIsArray,
isBodyJsonQueryEnabled = false,
handleChangeSelectedView,
}: ComputeDataNodeOptions): DataNode => ({
export const computeDataNode = (
key: string,
valueIsArray: boolean,
value: unknown,
nodeKey: string,
parentIsArray: boolean,
): DataNode => ({
key: uniqueId(),
title: (
<BodyTitleRenderer
@@ -68,30 +48,20 @@ export const computeDataNode = ({
nodeKey={nodeKey}
value={value}
parentIsArray={parentIsArray}
handleChangeSelectedView={handleChangeSelectedView}
/>
),
children: jsonToDataNodes(value as Record<string, unknown>, {
parentKey: valueIsArray
? `${nodeKey}${isBodyJsonQueryEnabled ? '[]' : '[*]'}`
: nodeKey,
parentIsArray: valueIsArray,
isBodyJsonQueryEnabled,
handleChangeSelectedView,
}),
children: jsonToDataNodes(
value as Record<string, unknown>,
valueIsArray ? `${nodeKey}[*]` : nodeKey,
valueIsArray,
),
});
export function jsonToDataNodes(
json: Record<string, unknown>,
options: JsonToDataNodesOptions = {},
parentKey = '',
parentIsArray = false,
): DataNode[] {
const {
parentKey = '',
parentIsArray = false,
isBodyJsonQueryEnabled = false,
handleChangeSelectedView,
} = options;
return Object.entries(json).map(([key, value]) => {
let nodeKey = parentKey || key;
if (parentIsArray) {
@@ -104,15 +74,7 @@ export function jsonToDataNodes(
if (parentIsArray) {
if (typeof value === 'object' && value !== null) {
return computeDataNode({
key,
valueIsArray,
value,
nodeKey,
parentIsArray,
isBodyJsonQueryEnabled,
handleChangeSelectedView,
});
return computeDataNode(key, valueIsArray, value, nodeKey, parentIsArray);
}
return {
@@ -123,31 +85,14 @@ export function jsonToDataNodes(
nodeKey={nodeKey}
value={value}
parentIsArray={parentIsArray}
handleChangeSelectedView={handleChangeSelectedView}
/>
),
children: jsonToDataNodes(
{},
{
parentKey: nodeKey,
parentIsArray: valueIsArray,
isBodyJsonQueryEnabled,
handleChangeSelectedView,
},
),
children: jsonToDataNodes({}, nodeKey, valueIsArray),
};
}
if (typeof value === 'object' && value !== null) {
return computeDataNode({
key,
valueIsArray,
value,
nodeKey,
parentIsArray,
isBodyJsonQueryEnabled,
handleChangeSelectedView,
});
return computeDataNode(key, valueIsArray, value, nodeKey, parentIsArray);
}
return {
key: uniqueId(),
@@ -157,7 +102,6 @@ export function jsonToDataNodes(
nodeKey={nodeKey}
value={value}
parentIsArray={parentIsArray}
handleChangeSelectedView={handleChangeSelectedView}
/>
),
};
@@ -179,7 +123,6 @@ export function flattenObject(obj: AnyObject, prefix = ''): AnyObject {
export const generateFieldKeyForArray = (
fieldKey: string,
dataType: DataTypes,
isBodyJsonQueryEnabled = false,
): string => {
let lastDotIndex = fieldKey.lastIndexOf('.');
let resultNodeKey = fieldKey;
@@ -195,16 +138,6 @@ export const generateFieldKeyForArray = (
newResultNodeKey = resultNodeKey.substring(0, lastDotIndex);
}
}
// When filtering for a value inside an array, the query builder expects the
// last array segment to be referenced without the trailing `[]`.
// Examples:
// - has(body.config.features, 'fast_checkout')
// - has(body.config.features[].items, 'pen')
// - has(body.config.features[].items[].variants, 'ballpen')
if (isBodyJsonQueryEnabled && newResultNodeKey.endsWith('[]')) {
newResultNodeKey = newResultNodeKey.slice(0, -2);
}
return `body.${newResultNodeKey}`;
};

View File

@@ -160,6 +160,55 @@
}
}
.lightMode {
.login-form-card {
background: var(--l2-background);
}
.login-error-container {
.error-content {
background: color-mix(in srgb, var(--danger-background) 10%, transparent);
border-color: color-mix(in srgb, var(--danger-background) 20%, transparent);
&__error-code {
color: var(--l2-foreground);
}
&__error-message {
color: var(--l1-foreground);
}
&__docs-button {
color: var(--l1-foreground);
border-color: var(--l1-border);
background: transparent;
&:hover {
color: var(--l2-foreground);
border-color: var(--l1-border);
background: transparent;
}
}
&__message-badge-label-text {
color: var(--l2-foreground);
}
&__message-item {
color: var(--l1-foreground);
&::before {
background: var(--l3-background);
}
}
&__scroll-hint-text {
color: var(--l2-foreground);
}
}
}
}
.password-label-container {
display: flex;
justify-content: space-between;

View File

@@ -10,6 +10,10 @@ export const Label = styled.label`
color: var(--l1-foreground);
margin-bottom: 12px;
display: block;
.lightMode & {
color: var(--text-ink-500);
}
`;
export const FormContainer = styled(Form)`
@@ -46,10 +50,20 @@ export const FormContainer = styled(Form)`
background: var(--l3-background) !important;
border-color: var(--l1-border) !important;
color: var(--l1-foreground) !important;
.lightMode & {
background: var(--bg-vanilla-200) !important;
border-color: var(--bg-vanilla-300) !important;
color: var(--text-ink-500) !important;
}
}
& .ant-input::placeholder {
color: var(--l3-foreground) !important;
.lightMode & {
color: var(--text-neutral-light-200) !important;
}
}
& .ant-input:focus,

View File

@@ -323,3 +323,50 @@
}
}
}
.lightMode {
.logs-list-view-container {
.logs-list-table-view-container {
table {
thead {
tr {
th {
color: var(--l1-foreground) !important;
}
border-bottom: 1px solid var(--l1-border) !important;
}
border-bottom: 1px solid var(--l1-border) !important;
background-color: var(--l1-background) !important;
tr {
&:hover {
background-color: var(--l3-background) !important;
}
}
}
tbody {
tr {
&:hover {
background-color: var(--l3-background) !important;
}
border-bottom: 1px solid var(--l1-border) !important;
}
}
}
.sticky-header-table-container {
&::-webkit-scrollbar-thumb {
background: var(--l3-background);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--l1-background);
}
}
}
}
}

View File

@@ -263,6 +263,24 @@
}
}
.lightMode {
.logs-explorer-views-container {
.views-tabs-container {
.views-tabs {
.selected_view {
background: white;
color: var(--text-robin-400);
border: 1px solid var(--bg-robin-400);
}
.selected_view::before {
background: var(--bg-robin-400);
}
}
}
}
}
.order-by-container {
display: flex;
align-items: center;

View File

@@ -1,83 +0,0 @@
.mcp-auth-card {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
padding: var(--padding-4) var(--padding-5);
border: 1px solid var(--l2-border);
border-radius: 8px;
background: var(--l2-background);
&__title {
display: flex;
align-items: center;
gap: var(--spacing-5);
font-size: var(--label-medium-500-font-size);
font-weight: var(--label-medium-500-font-weight);
color: var(--l1-foreground);
margin: 0;
}
&__description {
font-size: var(--font-size-xs);
color: var(--foreground);
line-height: var(--line-height-18);
margin: 0;
}
&__field {
display: flex;
flex-direction: column;
gap: var(--spacing-3);
}
&__field-label {
font-size: var(--font-size-xs);
font-weight: 500;
color: var(--foreground);
letter-spacing: 0.01em;
}
&__endpoint-value {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
padding: var(--padding-2) var(--padding-3);
border: 1px solid var(--l3-border);
border-radius: 6px;
background: var(--l3-background);
font-family: var(--font-family-sf-mono, monospace);
font-size: var(--paragraph-base-400-font-size);
color: var(--l1-foreground);
width: 100%;
justify-content: space-between;
}
&__cta-row {
display: flex;
align-items: center;
gap: var(--spacing-10);
flex-wrap: wrap;
}
&__helper-text {
font-size: var(--paragraph-small-400-font-size);
color: var(--foreground);
line-height: var(--paragraph-small-400-line-height);
}
&__info-banner {
display: flex;
align-items: flex-start;
gap: var(--spacing-2);
padding: var(--padding-2) var(--padding-3);
border-left: 3px solid var(--bg-robin-400);
background: var(--l3-background);
border-radius: 4px;
svg {
flex-shrink: 0;
color: var(--bg-robin-400);
margin-top: 2px;
}
}
}

View File

@@ -1,70 +0,0 @@
import { render, screen, userEvent } from 'tests/test-utils';
import AuthCard from './AuthCard';
const mockOnCopyInstanceUrl = jest.fn();
const mockOnCreateServiceAccount = jest.fn();
const defaultProps = {
instanceUrl: 'http://localhost',
onCopyInstanceUrl: mockOnCopyInstanceUrl,
onCreateServiceAccount: mockOnCreateServiceAccount,
};
describe('AuthCard', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('renders the instance URL', () => {
render(<AuthCard {...defaultProps} isAdmin />);
expect(screen.getByTestId('mcp-instance-url')).toHaveTextContent(
'http://localhost',
);
});
it('shows Create Service Account button for admin', () => {
render(<AuthCard {...defaultProps} isAdmin />);
expect(screen.getByText('Create service account')).toBeInTheDocument();
expect(
screen.queryByText(
'Only admins can create API keys. Ask your workspace admin for a key with read access, then paste it into the API Key field.',
),
).not.toBeInTheDocument();
});
it('shows info banner for non-admin', () => {
render(<AuthCard {...defaultProps} isAdmin={false} />);
expect(
screen.getByText(
'Only admins can create API keys. Ask your workspace admin for a key with read access, then paste it into the API Key field.',
),
).toBeInTheDocument();
expect(screen.queryByText('Create service account')).not.toBeInTheDocument();
});
it('calls onCopyInstanceUrl when copy button is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<AuthCard {...defaultProps} isAdmin />);
await user.click(
screen.getByRole('button', { name: 'Copy SigNoz instance URL' }),
);
expect(mockOnCopyInstanceUrl).toHaveBeenCalledTimes(1);
});
it('calls onCreateServiceAccount when admin clicks the CTA', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<AuthCard {...defaultProps} isAdmin />);
await user.click(screen.getByText('Create service account'));
expect(mockOnCreateServiceAccount).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,75 +0,0 @@
import { Badge, Button } from '@signozhq/ui';
import { Info, KeyRound } from '@signozhq/icons';
import CopyIconButton from '../CopyIconButton';
import './AuthCard.styles.scss';
interface AuthCardProps {
isAdmin: boolean;
instanceUrl: string;
onCopyInstanceUrl: () => void;
onCreateServiceAccount: () => void;
}
function AuthCard({
isAdmin,
instanceUrl,
onCopyInstanceUrl,
onCreateServiceAccount,
}: AuthCardProps): JSX.Element {
return (
<section className="mcp-auth-card">
<h3 className="mcp-auth-card__title">
<Badge color="secondary" variant="default">
2
</Badge>
Authenticate from your client
</h3>
<p className="mcp-auth-card__description">
On first connect, your client opens a SigNoz authorization page asking for
two values:
</p>
<div className="mcp-auth-card__field">
<span className="mcp-auth-card__field-label">SigNoz Instance URL</span>
<div className="mcp-auth-card__endpoint-value">
<span data-testid="mcp-instance-url">{instanceUrl}</span>
<CopyIconButton
ariaLabel="Copy SigNoz instance URL"
onCopy={onCopyInstanceUrl}
/>
</div>
</div>
<div className="mcp-auth-card__field">
<span className="mcp-auth-card__field-label">API Key</span>
{isAdmin ? (
<div className="mcp-auth-card__cta-row">
<Button
variant="solid"
color="primary"
prefix={<KeyRound size={14} />}
onClick={onCreateServiceAccount}
>
Create service account
</Button>
<span className="mcp-auth-card__helper-text">
Create a service account, then add a new key inside it - paste that key
into the API Key field.
</span>
</div>
) : (
<div className="mcp-auth-card__info-banner">
<Info size={14} />
<span className="mcp-auth-card__helper-text">
Only admins can create API keys. Ask your workspace admin for a key with
read access, then paste it into the API Key field.
</span>
</div>
)}
</div>
</section>
);
}
export default AuthCard;

View File

@@ -1,66 +0,0 @@
.mcp-client-tabs-root {
button[data-variant='primary'] {
border: none;
background: transparent;
box-shadow: none;
}
// Remove default tab content padding/margin — the card provides spacing.
--tab-content-padding: 0;
--tab-content-margin: var(--spacing-4) 0 0;
}
.mcp-client-tabs {
&__snippet-wrapper {
display: flex;
flex-direction: column;
gap: var(--spacing-6);
margin-top: var(--spacing-2);
.learn-more {
width: fit-content;
font-size: var(--font-size-xs);
}
}
&__install-row {
display: flex;
align-items: center;
gap: var(--spacing-10);
flex-wrap: wrap;
}
&__helper-text {
font-size: var(--paragraph-small-400-font-size);
color: var(--foreground);
line-height: var(--paragraph-small-400-line-height);
}
&__endpoint-value {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
padding: var(--padding-2) var(--padding-3);
border: 1px solid var(--l3-border);
border-radius: 6px;
background: var(--l3-background);
font-family: var(--font-family-sf-mono, monospace);
font-size: var(--paragraph-base-400-font-size);
color: var(--l1-foreground);
width: 100%;
justify-content: space-between;
}
&__snippet-pre {
margin: 0;
white-space: pre-wrap;
word-break: break-all;
}
&__instructions {
font-size: var(--font-size-xs);
color: var(--foreground);
line-height: var(--line-height-20);
margin: 0;
}
}

View File

@@ -1,108 +0,0 @@
import { render, screen, userEvent } from 'tests/test-utils';
import ClientTabs from './ClientTabs';
import { MCP_CLIENTS } from '../clients';
jest.mock('utils/navigation', () => ({
openInNewTab: jest.fn(),
}));
const mockOnTabChange = jest.fn();
const mockOnCopySnippet = jest.fn();
const mockOnInstallClick = jest.fn();
const mockOnDocsLinkClick = jest.fn();
const MCP_ENDPOINT = 'https://mcp.us.signoz.cloud/mcp';
const defaultProps = {
endpoint: MCP_ENDPOINT,
activeTab: MCP_CLIENTS[0].key,
onTabChange: mockOnTabChange,
onCopySnippet: mockOnCopySnippet,
onInstallClick: mockOnInstallClick,
onDocsLinkClick: mockOnDocsLinkClick,
};
describe('ClientTabs', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('renders a tab for each MCP client', () => {
render(<ClientTabs {...defaultProps} />);
MCP_CLIENTS.forEach((client) => {
expect(screen.getByText(client.label)).toBeInTheDocument();
});
});
it('renders the snippet for clients that provide one (Cursor)', () => {
render(<ClientTabs {...defaultProps} activeTab="cursor" />);
// The snippet is rendered inside a <pre> element; check its content
const snippetPre = document.querySelector('.mcp-client-tabs__snippet-pre');
expect(snippetPre).toBeInTheDocument();
expect(snippetPre?.textContent).toContain(MCP_ENDPOINT);
expect(snippetPre?.textContent).toContain('mcpServers');
});
it('renders endpoint block and instructions text for clients without a snippet (Claude Desktop)', () => {
render(<ClientTabs {...defaultProps} activeTab="claude-desktop" />);
const snippetPre = document.querySelector('.mcp-client-tabs__snippet-pre');
expect(snippetPre?.textContent).toBe(MCP_ENDPOINT);
expect(
screen.getByText(
'Open Claude Desktop, go to Settings → Connectors → Add custom connector, and paste the endpoint URL above. Claude Desktop does not read remote MCP servers from claude_desktop_config.json - the connector UI is the only supported path.',
),
).toBeInTheDocument();
});
it('shows enabled install button when endpoint is set (Cursor)', () => {
render(<ClientTabs {...defaultProps} activeTab="cursor" />);
const installBtn = screen.getByRole('button', {
name: 'Add to Cursor',
});
expect(installBtn).toBeEnabled();
});
it('shows disabled install button when endpoint is missing (Cursor)', () => {
render(<ClientTabs {...defaultProps} endpoint="" activeTab="cursor" />);
const installBtn = screen.getByRole('button', {
name: 'Add to Cursor',
});
expect(installBtn).toBeDisabled();
});
it('calls onCopySnippet with client key and snippet on copy', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const cursorClient = MCP_CLIENTS.find((c) => c.key === 'cursor')!;
render(<ClientTabs {...defaultProps} activeTab="cursor" />);
await user.click(
screen.getByRole('button', {
name: `Copy ${cursorClient.label} config`,
}),
);
expect(mockOnCopySnippet).toHaveBeenCalledWith(
'cursor',
cursorClient.snippet!(MCP_ENDPOINT),
);
});
it('copy button is disabled when no endpoint', () => {
render(<ClientTabs {...defaultProps} endpoint="" activeTab="cursor" />);
const cursorClient = MCP_CLIENTS.find((c) => c.key === 'cursor')!;
const copyBtn = screen.getByRole('button', {
name: `Copy ${cursorClient.label} config`,
});
expect(copyBtn).toBeDisabled();
});
});

View File

@@ -1,126 +0,0 @@
import { useMemo } from 'react';
import { Button, Tabs } from '@signozhq/ui';
import LearnMore from 'components/LearnMore/LearnMore';
import { Download } from '@signozhq/icons';
import { openInNewTab } from 'utils/navigation';
import CopyIconButton from '../CopyIconButton';
import { docsUrl, MCP_CLIENTS, McpClient } from '../clients';
import './ClientTabs.styles.scss';
const ENDPOINT_PLACEHOLDER = 'https://mcp.<region>.signoz.cloud/mcp';
interface ClientTabsProps {
endpoint: string;
activeTab: string;
onTabChange: (key: string) => void;
onCopySnippet: (clientKey: string, snippet: string) => void;
onInstallClick: (clientKey: string) => void;
onDocsLinkClick: (target: string) => void;
}
function ClientTabs({
endpoint,
activeTab,
onTabChange,
onCopySnippet,
onInstallClick,
onDocsLinkClick,
}: ClientTabsProps): JSX.Element {
const items = useMemo(
() =>
MCP_CLIENTS.map((client: McpClient) => {
const snippet = client.snippet
? client.snippet(endpoint || ENDPOINT_PLACEHOLDER)
: null;
const installHref =
client.installUrl && endpoint ? client.installUrl(endpoint) : null;
const installLabel = client.installLabel ?? `Add to ${client.label}`;
return {
key: client.key,
label: client.label,
children: (
<div className="mcp-client-tabs__snippet-wrapper">
{snippet !== null ? (
<div className="mcp-client-tabs__endpoint-value mcp-client-tabs__snippet">
<pre className="mcp-client-tabs__snippet-pre">{snippet}</pre>
<CopyIconButton
ariaLabel={`Copy ${client.label} config`}
disabled={!endpoint}
onCopy={(): void => onCopySnippet(client.key, snippet)}
/>
</div>
) : (
<>
<div className="mcp-client-tabs__endpoint-value mcp-client-tabs__snippet">
<pre className="mcp-client-tabs__snippet-pre">
{endpoint || ENDPOINT_PLACEHOLDER}
</pre>
<CopyIconButton
ariaLabel="Copy MCP endpoint"
disabled={!endpoint}
onCopy={(): void => onCopySnippet(client.key, endpoint)}
/>
</div>
<p className="mcp-client-tabs__instructions">
{client.instructions ?? ''}
</p>
</>
)}
{client.installUrl && (
<div className="mcp-client-tabs__install-row">
{installHref ? (
<Button
variant="solid"
color="primary"
prefix={<Download size={14} />}
onClick={(): void => {
onInstallClick(client.key);
openInNewTab(installHref);
}}
>
{installLabel}
</Button>
) : (
<Button
variant="solid"
color="primary"
disabled
prefix={<Download size={14} />}
>
{installLabel}
</Button>
)}
<span className="mcp-client-tabs__helper-text">
Or copy the config below for manual setup.
</span>
</div>
)}
<LearnMore
text={`${client.label} setup docs`}
url={docsUrl(client.docsPath)}
onClick={(): void => onDocsLinkClick(`client-${client.key}`)}
/>
</div>
),
};
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[endpoint, onCopySnippet, onInstallClick, onDocsLinkClick],
);
return (
<Tabs
className="mcp-client-tabs-root"
value={activeTab}
onChange={onTabChange}
items={items}
/>
);
}
export default ClientTabs;

View File

@@ -1,5 +0,0 @@
.mcp-copy-btn {
&:hover {
background-color: var(--l3-background-hover) !important;
}
}

View File

@@ -1,40 +0,0 @@
import { Button, Tooltip, TooltipProvider } from '@signozhq/ui';
import { Copy } from '@signozhq/icons';
import './CopyIconButton.styles.scss';
interface CopyIconButtonProps {
ariaLabel: string;
onCopy: () => void;
disabled?: boolean;
}
function CopyIconButton({
ariaLabel,
onCopy,
disabled = false,
}: CopyIconButtonProps): JSX.Element {
const tooltipTitle = disabled
? 'Enter your Cloud region first'
: 'Copy to clipboard';
return (
<TooltipProvider>
<Tooltip title={tooltipTitle}>
<span>
<Button
color="secondary"
variant="ghost"
size="icon"
aria-label={ariaLabel}
disabled={disabled}
className="mcp-copy-btn"
prefix={<Copy size={14} />}
onClick={onCopy}
/>
</span>
</Tooltip>
</TooltipProvider>
);
}
export default CopyIconButton;

View File

@@ -1,60 +0,0 @@
.mcp-settings {
display: flex;
flex-direction: column;
gap: var(--spacing-12);
padding: var(--padding-8) var(--padding-2) var(--padding-6) var(--padding-4);
max-width: 880px;
margin: 0 auto;
width: 100%;
&__header {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
&__header-title {
font-size: var(--label-large-500-font-size);
font-weight: var(--label-large-500-font-weight);
color: var(--l1-foreground);
letter-spacing: -0.09px;
line-height: var(--line-height-normal);
margin: 0;
}
&__header-subtitle {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--foreground);
letter-spacing: -0.07px;
line-height: var(--paragraph-base-400-line-height);
margin: 0;
}
}
.mcp-settings__card {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
padding: var(--padding-4) var(--padding-5);
border: 1px solid var(--l2-border);
border-radius: 8px;
background: var(--l2-background);
&-title {
display: flex;
align-items: center;
gap: var(--spacing-5);
font-size: var(--label-medium-500-font-size);
font-weight: var(--label-medium-500-font-weight);
color: var(--l1-foreground);
margin: 0;
}
&-description {
font-size: var(--font-size-xs);
color: var(--foreground);
line-height: var(--line-height-18);
margin: 0;
}
}

View File

@@ -1,161 +0,0 @@
import { render, screen, userEvent } from 'tests/test-utils';
import MCPServerSettings from './MCPServerSettings';
const mockLogEvent = jest.fn();
const mockCopyToClipboard = jest.fn();
const mockHistoryPush = jest.fn();
const mockUseGetGlobalConfig = jest.fn();
const mockToastSuccess = jest.fn();
const mockToastWarning = jest.fn();
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: (...args: unknown[]): unknown => mockLogEvent(...args),
}));
jest.mock('api/generated/services/global', () => ({
useGetGlobalConfig: (...args: unknown[]): unknown =>
mockUseGetGlobalConfig(...args),
}));
jest.mock('react-use', () => ({
__esModule: true,
useCopyToClipboard: (): [unknown, jest.Mock] => [null, mockCopyToClipboard],
}));
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: {
success: (...args: unknown[]): unknown => mockToastSuccess(...args),
warning: (...args: unknown[]): unknown => mockToastWarning(...args),
},
}));
jest.mock('lib/history', () => ({
__esModule: true,
default: {
push: (...args: unknown[]): unknown => mockHistoryPush(...args),
location: { pathname: '/', search: '', hash: '', state: null },
},
}));
jest.mock('utils/basePath', () => ({
getBaseUrl: (): string => 'http://localhost',
getBasePath: (): string => '/',
withBasePath: (p: string): string => p,
}));
const MCP_URL = 'https://mcp.us.signoz.cloud/mcp';
function setupGlobalConfig({ mcpUrl }: { mcpUrl: string | null }): void {
mockUseGetGlobalConfig.mockReturnValue({
data: { data: { mcp_url: mcpUrl, ingestion_url: '' }, status: 'success' },
isLoading: false,
});
}
describe('MCPServerSettings', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('shows loading spinner while config is loading', () => {
mockUseGetGlobalConfig.mockReturnValue({
data: undefined,
isLoading: true,
});
render(<MCPServerSettings />);
expect(document.querySelector('.ant-spin-spinning')).toBeInTheDocument();
expect(screen.queryByTestId('mcp-settings')).not.toBeInTheDocument();
});
it('shows fallback page when mcp_url is not configured', () => {
setupGlobalConfig({ mcpUrl: null });
render(<MCPServerSettings />);
expect(
screen.getByText('MCP Server is available on SigNoz'),
).toBeInTheDocument();
expect(screen.queryByTestId('mcp-settings')).not.toBeInTheDocument();
});
it('renders main settings page when mcp_url is present', () => {
setupGlobalConfig({ mcpUrl: MCP_URL });
render(<MCPServerSettings />);
expect(screen.getByTestId('mcp-settings')).toBeInTheDocument();
expect(screen.getByText('SigNoz MCP Server')).toBeInTheDocument();
expect(screen.getByText('Configure your client')).toBeInTheDocument();
expect(screen.getByText('Authenticate from your client')).toBeInTheDocument();
});
it('fires PAGE_VIEWED analytics event on mount', () => {
setupGlobalConfig({ mcpUrl: MCP_URL });
render(<MCPServerSettings />, undefined, { role: 'ADMIN' });
expect(mockLogEvent).toHaveBeenCalledWith('MCP Settings: Page viewed', {
role: 'ADMIN',
});
});
it('admin sees the Create Service Account CTA', () => {
setupGlobalConfig({ mcpUrl: MCP_URL });
render(<MCPServerSettings />, undefined, { role: 'ADMIN' });
expect(screen.getByText('Create service account')).toBeInTheDocument();
expect(
screen.queryByText(
'Only admins can create API keys. Ask your workspace admin for a key with read access, then paste it into the API Key field.',
),
).not.toBeInTheDocument();
});
it('non-admin sees an info banner instead of the CTA', () => {
setupGlobalConfig({ mcpUrl: MCP_URL });
render(<MCPServerSettings />, undefined, { role: 'VIEWER' });
expect(
screen.getByText(
'Only admins can create API keys. Ask your workspace admin for a key with read access, then paste it into the API Key field.',
),
).toBeInTheDocument();
expect(screen.queryByText('Create service account')).not.toBeInTheDocument();
});
it('navigates to service accounts when admin clicks Create CTA', async () => {
setupGlobalConfig({ mcpUrl: MCP_URL });
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<MCPServerSettings />, undefined, { role: 'ADMIN' });
await user.click(screen.getByText('Create service account'));
expect(mockHistoryPush).toHaveBeenCalledWith(
'/settings/service-accounts?create-sa=true',
);
});
it('copies instance URL and shows success toast', async () => {
setupGlobalConfig({ mcpUrl: MCP_URL });
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<MCPServerSettings />);
await user.click(
screen.getByRole('button', { name: 'Copy SigNoz instance URL' }),
);
expect(mockCopyToClipboard).toHaveBeenCalledWith('http://localhost');
expect(mockToastSuccess).toHaveBeenCalledWith(
'Instance URL copied to clipboard',
);
});
});

View File

@@ -1,144 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
import { useGetGlobalConfig } from 'api/generated/services/global';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
import { getBaseUrl } from 'utils/basePath';
import { Badge, toast } from '@signozhq/ui';
import Spinner from 'components/Spinner';
import AuthCard from './AuthCard/AuthCard';
import ClientTabs from './ClientTabs/ClientTabs';
import NotCloudFallback from './NotCloudFallback/NotCloudFallback';
import UseCasesCard from './UseCasesCard/UseCasesCard';
import { MCP_CLIENTS } from './clients';
import './MCPServerSettings.styles.scss';
const ANALYTICS = {
PAGE_VIEWED: 'MCP Settings: Page viewed',
CREATE_SA_CLICKED: 'MCP Settings: Create service account clicked',
CLIENT_TAB_SELECTED: 'MCP Settings: Client tab selected',
SNIPPET_COPIED: 'MCP Settings: Client snippet copied',
ONE_CLICK_INSTALL_CLICKED: 'MCP Settings: One-click install clicked',
INSTANCE_URL_COPIED: 'MCP Settings: Instance URL copied',
DOCS_LINK_CLICKED: 'MCP Settings: Docs link clicked',
} as const;
function MCPServerSettings(): JSX.Element {
const { user } = useAppContext();
const [, copyToClipboard] = useCopyToClipboard();
const isAdmin = user.role === USER_ROLES.ADMIN;
const instanceUrl = getBaseUrl();
const { data: globalConfig, isLoading: isConfigLoading } =
useGetGlobalConfig();
const endpoint = globalConfig?.data?.mcp_url ?? '';
const [activeTab, setActiveTab] = useState<string>(MCP_CLIENTS[0]?.key ?? '');
useEffect(() => {
void logEvent(ANALYTICS.PAGE_VIEWED, {
role: user.role,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleCopySnippet = useCallback(
(clientKey: string, snippet: string) => {
if (!endpoint) {
toast.warning('Enter your Cloud region before copying');
return;
}
copyToClipboard(snippet);
toast.success('Snippet copied to clipboard');
void logEvent(ANALYTICS.SNIPPET_COPIED, { client: clientKey });
},
[endpoint, copyToClipboard],
);
const handleCreateServiceAccount = useCallback(() => {
void logEvent(ANALYTICS.CREATE_SA_CLICKED, {});
history.push(
`${ROUTES.SERVICE_ACCOUNTS_SETTINGS}?${SA_QUERY_PARAMS.CREATE_SA}=true`,
);
}, []);
const handleCopyInstanceUrl = useCallback(() => {
copyToClipboard(instanceUrl);
toast.success('Instance URL copied to clipboard');
void logEvent(ANALYTICS.INSTANCE_URL_COPIED, {});
}, [copyToClipboard, instanceUrl]);
const handleDocsLinkClick = useCallback((target: string) => {
void logEvent(ANALYTICS.DOCS_LINK_CLICKED, { target });
}, []);
const handleInstallClick = useCallback((clientKey: string) => {
void logEvent(ANALYTICS.ONE_CLICK_INSTALL_CLICKED, { client: clientKey });
}, []);
const handleTabChange = useCallback((key: string) => {
setActiveTab(key);
void logEvent(ANALYTICS.CLIENT_TAB_SELECTED, { client: key });
}, []);
if (isConfigLoading) {
return <Spinner tip="Loading..." height="70vh" />;
}
if (!endpoint) {
return <NotCloudFallback />;
}
return (
<div className="mcp-settings" data-testid="mcp-settings">
<header className="mcp-settings__header">
<h1 className="mcp-settings__header-title">SigNoz MCP Server</h1>
<p className="mcp-settings__header-subtitle">
Connect AI assistants like Claude, Cursor, VS Code, and Codex to your
SigNoz data via the Model Context Protocol. Authenticate from your MCP
client with a service-account API key.
</p>
</header>
<section className="mcp-settings__card">
<h3 className="mcp-settings__card-title">
<Badge color="secondary" variant="default">
1
</Badge>
Configure your client
</h3>
<p className="mcp-settings__card-description">
Add SigNoz to your MCP client. Use a one-click install where available, or
copy the config for manual setup. On first connect, the client will open a
SigNoz authorization page - use the instance URL and API key from step 2.
</p>
<ClientTabs
endpoint={endpoint}
activeTab={activeTab}
onTabChange={handleTabChange}
onCopySnippet={handleCopySnippet}
onInstallClick={handleInstallClick}
onDocsLinkClick={handleDocsLinkClick}
/>
</section>
<AuthCard
isAdmin={isAdmin}
instanceUrl={instanceUrl}
onCopyInstanceUrl={handleCopyInstanceUrl}
onCreateServiceAccount={handleCreateServiceAccount}
/>
<UseCasesCard onDocsLinkClick={handleDocsLinkClick} />
</div>
);
}
export default MCPServerSettings;

View File

@@ -1,41 +0,0 @@
.not-cloud-fallback {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: 60vh;
&__content {
display: flex;
flex-direction: column;
gap: var(--spacing-6);
padding: var(--padding-6);
border: 1px dashed var(--l2-border);
border-radius: 8px;
background: var(--l2-background);
max-width: 50%;
.learn-more {
width: fit-content;
font-size: var(--font-size-xs);
}
}
&__title {
display: flex;
align-items: center;
gap: var(--spacing-2);
font-size: var(--label-large-500-font-size);
font-weight: var(--label-large-500-font-weight);
color: var(--l1-foreground);
margin: 0;
}
&__body {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--foreground);
line-height: var(--paragraph-base-400-line-height);
margin: 0;
}
}

View File

@@ -1,35 +0,0 @@
import { useCallback } from 'react';
import logEvent from 'api/common/logEvent';
import LearnMore from 'components/LearnMore/LearnMore';
import './NotCloudFallback.styles.scss';
import { MCP_DOCS_URL } from '../clients';
const DOCS_LINK_EVENT = 'MCP Settings: Docs link clicked';
function NotCloudFallback(): JSX.Element {
const handleDocsClick = useCallback(() => {
void logEvent(DOCS_LINK_EVENT, { target: 'fallback' });
}, []);
return (
<div className="not-cloud-fallback">
<div className="not-cloud-fallback__content">
<h2 className="not-cloud-fallback__title">
MCP Server is available on SigNoz
</h2>
<p className="not-cloud-fallback__body">
Users can follow the docs to setup the MCP server against their SigNoz
instance.
</p>
<LearnMore
text="View MCP Server docs"
url={MCP_DOCS_URL}
onClick={handleDocsClick}
/>
</div>
</div>
);
}
export default NotCloudFallback;

View File

@@ -1,35 +0,0 @@
.mcp-use-cases-card {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
padding: var(--padding-4) var(--padding-5);
border: 1px solid var(--l2-border);
border-radius: 8px;
background: var(--l2-background);
&__title {
font-size: var(--label-medium-500-font-size);
font-weight: var(--label-medium-500-font-weight);
color: var(--l1-foreground);
margin: 0;
}
.learn-more {
width: fit-content;
font-size: var(--font-size-xs);
}
&__list {
display: flex;
flex-direction: column;
gap: var(--spacing-1);
padding-left: var(--padding-4);
margin: 0;
li {
font-size: var(--font-size-xs);
color: var(--l1-foreground);
line-height: var(--line-height-20);
}
}
}

View File

@@ -1,35 +0,0 @@
import { useCallback } from 'react';
import LearnMore from 'components/LearnMore/LearnMore';
import { MCP_USE_CASES_URL } from '../clients';
import './UseCasesCard.styles.scss';
interface UseCasesCardProps {
onDocsLinkClick: (target: string) => void;
}
function UseCasesCard({ onDocsLinkClick }: UseCasesCardProps): JSX.Element {
const handleClick = useCallback(
() => onDocsLinkClick('use-cases'),
[onDocsLinkClick],
);
return (
<section className="mcp-use-cases-card">
<h3 className="mcp-use-cases-card__title">What you can do with it</h3>
<ul className="mcp-use-cases-card__list">
<li>Ask your AI assistant to investigate a spiking error rate.</li>
<li>Debug a slow service by walking through recent traces.</li>
<li>Summarize an alert and suggest likely root causes.</li>
<li>Generate dashboards or queries from a natural-language description.</li>
</ul>
<LearnMore
text="See more use cases"
url={MCP_USE_CASES_URL}
onClick={handleClick}
/>
</section>
);
}
export default UseCasesCard;

View File

@@ -1,108 +0,0 @@
import { DOCS_BASE_URL } from 'constants/app';
export interface McpClient {
key: string;
label: string;
docsPath: string;
snippet: ((endpoint: string) => string) | null;
instructions?: string;
installUrl?: (endpoint: string) => string;
installLabel?: string;
}
function b64url(input: string): string {
if (typeof btoa === 'function') {
return btoa(input);
}
// fallback for non-browser TS contexts (never hit at runtime)
return Buffer.from(input, 'utf8').toString('base64');
}
export const MCP_CLIENTS: McpClient[] = [
{
key: 'cursor',
label: 'Cursor',
docsPath: '/docs/ai/signoz-mcp-server/#cursor',
snippet: (endpoint): string =>
JSON.stringify(
{
mcpServers: {
signoz: {
url: endpoint,
},
},
},
null,
2,
),
installUrl: (endpoint): string => {
const config = b64url(JSON.stringify({ url: endpoint }));
return `cursor://anysphere.cursor-deeplink/mcp/install?name=SigNoz&config=${config}`;
},
installLabel: 'Add to Cursor',
},
{
key: 'claude-code',
label: 'Claude Code',
docsPath: '/docs/ai/signoz-mcp-server/#claude-code',
snippet: (endpoint): string =>
`claude mcp add --scope user --transport http signoz ${endpoint}`,
},
{
key: 'vscode',
label: 'VS Code',
docsPath: '/docs/ai/signoz-mcp-server/#vs-code',
snippet: (endpoint): string =>
JSON.stringify(
{
servers: {
signoz: {
type: 'http',
url: endpoint,
},
},
},
null,
2,
),
installUrl: (endpoint): string => {
const payload = encodeURIComponent(
JSON.stringify({
name: 'signoz',
config: { type: 'http', url: endpoint },
}),
);
return `vscode:mcp/install?${payload}`;
},
installLabel: 'Add to VS Code',
},
{
key: 'claude-desktop',
label: 'Claude Desktop',
docsPath: '/docs/ai/signoz-mcp-server/#claude-desktop',
snippet: null,
instructions:
'Open Claude Desktop, go to Settings → Connectors → Add custom connector, and paste the endpoint URL above. Claude Desktop does not read remote MCP servers from claude_desktop_config.json - the connector UI is the only supported path.',
},
{
key: 'codex',
label: 'Codex',
docsPath: '/docs/ai/signoz-mcp-server/#codex',
snippet: (endpoint): string => `codex mcp add signoz --url ${endpoint}`,
},
{
key: 'other',
label: 'Other',
docsPath: '/docs/ai/signoz-mcp-server/',
snippet: null,
instructions:
'Most MCP clients that support remote HTTP servers will accept the endpoint URL above. Add it as a new MCP server in your client and paste your SigNoz API key when the client prompts for authentication. See the docs for client-specific instructions.',
},
];
export function docsUrl(path: string): string {
return `${DOCS_BASE_URL}${path}`;
}
export const MCP_DOCS_URL = `${DOCS_BASE_URL}/docs/ai/signoz-mcp-server/`;
export const MCP_USE_CASES_URL = `${DOCS_BASE_URL}/docs/ai/use-cases/`;

View File

@@ -179,6 +179,16 @@
}
}
.lightMode {
.metrics-explorer-explore-container {
.explore-tabs {
.selected-view {
background: var(--l3-background);
}
}
}
}
.dashboards-and-alerts-popover-container {
display: flex;
gap: 16px;

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