mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-27 14:10:30 +01:00
Compare commits
2 Commits
infraM/v2_
...
chore/base
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af1031a1cd | ||
|
|
1f602a216e |
@@ -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
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -16,6 +16,5 @@
|
||||
"roles": "Roles",
|
||||
"role_details": "Role Details",
|
||||
"members": "Members",
|
||||
"service_accounts": "Service Accounts",
|
||||
"mcp_server": "MCP Server"
|
||||
"service_accounts": "Service Accounts"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -16,6 +16,5 @@
|
||||
"roles": "Roles",
|
||||
"role_details": "Role Details",
|
||||
"members": "Members",
|
||||
"service_accounts": "Service Accounts",
|
||||
"mcp_server": "MCP Server"
|
||||
"service_accounts": "Service Accounts"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
/**
|
||||
|
||||
25
frontend/src/api/globalConfig/getGlobalConfig.ts
Normal file
25
frontend/src/api/globalConfig/getGlobalConfig.ts
Normal 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;
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
.dropdown-button {
|
||||
color: var(--l1-foreground);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dropdown-button--dark {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -12,12 +12,6 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__tab-group {
|
||||
[data-slot='toggle-group'] {
|
||||
border-radius: 2px;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -157,3 +157,9 @@
|
||||
.view-all-drawer {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.ant-table {
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,3 +61,11 @@
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.dashboard-breadcrumbs {
|
||||
.dashboard-btn {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,3 +176,15 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.info-text {
|
||||
color: var(--bg-robin-600) !important;
|
||||
}
|
||||
|
||||
.info-link-container {
|
||||
.anticon {
|
||||
color: var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
212
frontend/src/container/ListOfDashboard/SearchFilter/index.tsx
Normal file
212
frontend/src/container/ListOfDashboard/SearchFilter/index.tsx
Normal 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;
|
||||
@@ -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;
|
||||
`;
|
||||
18
frontend/src/container/ListOfDashboard/SearchFilter/types.ts
Normal file
18
frontend/src/container/ListOfDashboard/SearchFilter/types.ts
Normal 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[] | [];
|
||||
}
|
||||
153
frontend/src/container/ListOfDashboard/SearchFilter/utils.ts
Normal file
153
frontend/src/container/ListOfDashboard/SearchFilter/utils.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) && (
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export const DROPDOWN_KEY = {
|
||||
FILTER_IN: 'filterIn',
|
||||
FILTER_OUT: 'filterOut',
|
||||
GROUP_BY: 'groupBy',
|
||||
};
|
||||
|
||||
@@ -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}`;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -1,5 +0,0 @@
|
||||
.mcp-copy-btn {
|
||||
&:hover {
|
||||
background-color: var(--l3-background-hover) !important;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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/`;
|
||||
@@ -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
Reference in New Issue
Block a user