mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-07 00:40:42 +00:00
Compare commits
10 Commits
feat/input
...
fix/genera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7ef3b0a67 | ||
|
|
cbab3ca8d7 | ||
|
|
ab28e69397 | ||
|
|
d98fca08dd | ||
|
|
648c8d0740 | ||
|
|
e6c19dd15a | ||
|
|
ccfe8114f1 | ||
|
|
b87e2b6375 | ||
|
|
b45c4828ac | ||
|
|
40d3f9c81d |
6
.github/workflows/e2eci.yaml
vendored
6
.github/workflows/e2eci.yaml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
FORCE: ${{ contains(github.event.pull_request.labels.*.name, 'safe-to-e2e') }}
|
||||
run: |
|
||||
CHANGED_FILES=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename | select(startswith("tests/integration/") | not)')
|
||||
CHANGED_FILES=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename | select((startswith("tests/integration/") or startswith("frontend/src/api/generated/")) | not)')
|
||||
PATTERN='^(frontend/|tests/|\.github/workflows/e2eci\.yaml$|cmd/enterprise/Dockerfile\.with-web\.integration$)'
|
||||
|
||||
if [ "$FORCE" = "true" ]; then
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
- name: node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
node-version: "22"
|
||||
- name: install-pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
- name: node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
node-version: "22"
|
||||
- name: install-pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
|
||||
@@ -552,26 +552,47 @@ components:
|
||||
required:
|
||||
- apiKey
|
||||
type: object
|
||||
AlertmanagertypesChannelJiraConfig:
|
||||
AlertmanagertypesChannelJiraAuth:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelJiraCloudAuth'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelJiraSelfHostedAuth'
|
||||
type: object
|
||||
AlertmanagertypesChannelJiraCloudAuth:
|
||||
properties:
|
||||
apiToken:
|
||||
description: The Atlassian API token paired with email. A secret; returned
|
||||
by create, update and fetch by ID, never by list.
|
||||
format: password
|
||||
type: string
|
||||
email:
|
||||
description: The Atlassian account email used for basic auth.
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
- apiToken
|
||||
type: object
|
||||
AlertmanagertypesChannelJiraConfig:
|
||||
properties:
|
||||
auth:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelJiraAuth'
|
||||
description: 'The credentials: email and apiToken when deployment is cloud,
|
||||
pat when deployment is selfhosted.'
|
||||
customFields:
|
||||
additionalProperties: {}
|
||||
description: Extra issue fields keyed by Jira field ID. Stored, but not
|
||||
currently included in the issue Jira receives.
|
||||
nullable: true
|
||||
type: object
|
||||
deployment:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AlertmanagertypesJiraDeployment'
|
||||
description: 'The Jira edition: cloud for an Atlassian-hosted site, selfhosted
|
||||
for Jira Server or Data Center.'
|
||||
description:
|
||||
description: Alertmanager template for the issue description, rendered as
|
||||
markdown; defaults to the SigNoz Jira default.
|
||||
type: string
|
||||
email:
|
||||
description: The Atlassian account email used for basic auth.
|
||||
type: string
|
||||
issueType:
|
||||
description: The issue type name, such as Bug.
|
||||
type: string
|
||||
@@ -604,8 +625,8 @@ components:
|
||||
nullable: true
|
||||
type: boolean
|
||||
site:
|
||||
description: The base URL of the Jira Cloud site, for example https://example.atlassian.net.
|
||||
Only Jira Cloud is supported.
|
||||
description: The base URL of the Jira site, for example https://example.atlassian.net
|
||||
or https://jira.example.com.
|
||||
type: string
|
||||
summary:
|
||||
description: Alertmanager template for the issue summary; defaults to the
|
||||
@@ -615,11 +636,21 @@ components:
|
||||
description: The resolution name that stops an issue from being reopened.
|
||||
type: string
|
||||
required:
|
||||
- deployment
|
||||
- site
|
||||
- project
|
||||
- issueType
|
||||
- email
|
||||
- apiToken
|
||||
- auth
|
||||
type: object
|
||||
AlertmanagertypesChannelJiraSelfHostedAuth:
|
||||
properties:
|
||||
pat:
|
||||
description: The Jira personal access token, sent as a Bearer header. A
|
||||
secret; returned by create, update and fetch by ID, never by list.
|
||||
format: password
|
||||
type: string
|
||||
required:
|
||||
- pat
|
||||
type: object
|
||||
AlertmanagertypesChannelKind:
|
||||
enum:
|
||||
@@ -1155,6 +1186,11 @@ components:
|
||||
description: Comma-separated tags attached to the alert.
|
||||
type: string
|
||||
type: object
|
||||
AlertmanagertypesJiraDeployment:
|
||||
enum:
|
||||
- cloud
|
||||
- selfhosted
|
||||
type: string
|
||||
AlertmanagertypesJiraReceiverConfig:
|
||||
properties:
|
||||
custom_fields:
|
||||
@@ -1162,6 +1198,10 @@ components:
|
||||
description: Extra issue fields keyed by Jira field ID. Stored, but not
|
||||
currently included in the issue Jira receives.
|
||||
type: object
|
||||
deployment:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AlertmanagertypesJiraDeployment'
|
||||
description: The Jira edition, cloud or selfhosted; defaults to cloud.
|
||||
description:
|
||||
description: Alertmanager template for the issue description.
|
||||
type: string
|
||||
@@ -1169,7 +1209,8 @@ components:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ConfigHTTPClientConfig'
|
||||
description: HTTP client settings; basic auth carries the account email
|
||||
and API token.
|
||||
and API token on cloud, authorization carries the Bearer personal access
|
||||
token on self-hosted.
|
||||
issue_type:
|
||||
description: The issue type name.
|
||||
type: string
|
||||
@@ -1199,7 +1240,7 @@ components:
|
||||
send_resolved:
|
||||
type: boolean
|
||||
site:
|
||||
description: The Jira Cloud base URL.
|
||||
description: The Jira base URL.
|
||||
type: string
|
||||
summary:
|
||||
description: Alertmanager template for the issue summary.
|
||||
@@ -4027,7 +4068,9 @@ components:
|
||||
the query they draw.
|
||||
cursor:
|
||||
description: The opaque nextCursor value from a previous raw result, used
|
||||
to fetch the next page.
|
||||
to fetch the next page. Honored only by raw requests whose order starts
|
||||
with timestamp, for logs followed by id in the same direction; every other
|
||||
request ignores it.
|
||||
type: string
|
||||
disabled:
|
||||
description: When true, the result is excluded from the response; the query
|
||||
@@ -4064,10 +4107,11 @@ components:
|
||||
are replaced by group-by label values.
|
||||
type: string
|
||||
limit:
|
||||
description: The maximum number of rows for raw requests, or of series or
|
||||
rows for grouped aggregations, keeping the top entries by order. When
|
||||
0, aggregations are unlimited while raw requests ordered by timestamp
|
||||
return a page of 10000 rows; follow nextCursor for the rest.
|
||||
description: The maximum number of rows for raw and trace requests, or of
|
||||
series or rows for grouped scalar and time_series requests, keeping the
|
||||
top entries by order. When 0, scalar and time_series requests are unlimited,
|
||||
raw requests ordered by timestamp return a page of 10000 rows to continue
|
||||
with nextCursor, and other raw and trace requests return 100 rows.
|
||||
type: integer
|
||||
limitBy:
|
||||
allOf:
|
||||
@@ -4079,8 +4123,11 @@ components:
|
||||
operators and selectedQueryName refer to it.
|
||||
type: string
|
||||
offset:
|
||||
description: The number of rows to skip before returning results. Prefer
|
||||
cursor for raw requests.
|
||||
description: The number of rows for raw and trace requests, or of ordered
|
||||
series or rows for scalar and time_series requests, to skip before the
|
||||
page starts. Requires limit. On raw requests the skip counts from where
|
||||
a cursor resumes, and the skipped rows are still read, so prefer cursor
|
||||
for paging.
|
||||
type: integer
|
||||
order:
|
||||
description: The sort order of the result. Order by a group-by field, an
|
||||
@@ -9562,9 +9609,10 @@ components:
|
||||
Querybuildertypesv5AggregationMeta:
|
||||
properties:
|
||||
buckets:
|
||||
description: The ascending upper bounds of the value bands, set only for
|
||||
heatmap results; each point then carries one count per bound plus a trailing
|
||||
overflow count in values.
|
||||
description: The ascending upper bounds of the value buckets, set for heatmap
|
||||
and distribution results; each point then carries one count per bound
|
||||
plus a trailing overflow count in values. The first bucket starts at -Inf
|
||||
when its bound is at or under zero, else at zero.
|
||||
items:
|
||||
format: double
|
||||
type: number
|
||||
@@ -9719,6 +9767,101 @@ components:
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
Querybuildertypesv5Distribution:
|
||||
properties:
|
||||
counts:
|
||||
description: One count per bound in meta.buckets plus a trailing overflow
|
||||
count, summed over every step of the requested time range.
|
||||
items:
|
||||
format: double
|
||||
type: number
|
||||
nullable: true
|
||||
type: array
|
||||
labels:
|
||||
description: The group-by field values that identify this distribution.
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Label'
|
||||
type: array
|
||||
required:
|
||||
- counts
|
||||
type: object
|
||||
Querybuildertypesv5DistributionAggregation:
|
||||
properties:
|
||||
alias:
|
||||
description: The alias of the aggregation, or the internal name __result_N,
|
||||
with N its index, when no alias was given.
|
||||
type: string
|
||||
distributions:
|
||||
description: One distribution per group-by combination.
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Distribution'
|
||||
nullable: true
|
||||
type: array
|
||||
index:
|
||||
description: The zero-based position of the aggregation in the query.
|
||||
type: integer
|
||||
meta:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5AggregationMeta'
|
||||
description: Display metadata for the aggregation, including the bucket
|
||||
upper bounds the counts are positional against.
|
||||
percentiles:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5DistributionPercentiles'
|
||||
description: Percentiles over the counts of every distribution added together,
|
||||
estimated the way histogram_quantile does by interpolating linearly within
|
||||
the bucket each rank falls in. Omitted when nothing was counted or meta.buckets
|
||||
is empty.
|
||||
required:
|
||||
- index
|
||||
- alias
|
||||
- distributions
|
||||
type: object
|
||||
Querybuildertypesv5DistributionData:
|
||||
properties:
|
||||
aggregations:
|
||||
description: One entry per aggregation of the query. The order is not guaranteed;
|
||||
match entries to aggregations by index.
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5DistributionAggregation'
|
||||
nullable: true
|
||||
type: array
|
||||
queryName:
|
||||
description: The name of the query that produced this result.
|
||||
type: string
|
||||
required:
|
||||
- queryName
|
||||
- aggregations
|
||||
type: object
|
||||
Querybuildertypesv5DistributionPercentiles:
|
||||
properties:
|
||||
p50:
|
||||
description: The 50th percentile.
|
||||
format: double
|
||||
type: number
|
||||
p75:
|
||||
description: The 75th percentile.
|
||||
format: double
|
||||
type: number
|
||||
p90:
|
||||
description: The 90th percentile.
|
||||
format: double
|
||||
type: number
|
||||
p95:
|
||||
description: The 95th percentile.
|
||||
format: double
|
||||
type: number
|
||||
p99:
|
||||
description: The 99th percentile.
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- p50
|
||||
- p75
|
||||
- p90
|
||||
- p95
|
||||
- p99
|
||||
type: object
|
||||
Querybuildertypesv5ExecStats:
|
||||
description: 'Execution cost of the request: rows and bytes scanned and execution
|
||||
time. A measure of cost, not of whether data exists.'
|
||||
@@ -10102,6 +10245,10 @@ components:
|
||||
description: The unique name of the formula, such as F1, used by selectedQueryName
|
||||
and by panels. Referencing a formula from another formula is not reliable.
|
||||
type: string
|
||||
offset:
|
||||
description: The number of ordered series or rows to skip before the page
|
||||
starts. Requires limit.
|
||||
type: integer
|
||||
order:
|
||||
description: The sort order of the formula result.
|
||||
items:
|
||||
@@ -10127,7 +10274,9 @@ components:
|
||||
the query they draw.
|
||||
cursor:
|
||||
description: The opaque nextCursor value from a previous raw result, used
|
||||
to fetch the next page.
|
||||
to fetch the next page. Honored only by raw requests whose order starts
|
||||
with timestamp, for logs followed by id in the same direction; every other
|
||||
request ignores it.
|
||||
type: string
|
||||
disabled:
|
||||
description: When true, the result is excluded from the response; the query
|
||||
@@ -10163,10 +10312,11 @@ components:
|
||||
are replaced by group-by label values.
|
||||
type: string
|
||||
limit:
|
||||
description: The maximum number of rows for raw requests, or of series or
|
||||
rows for grouped aggregations, keeping the top entries by order. When
|
||||
0, aggregations are unlimited while raw requests ordered by timestamp
|
||||
return a page of 10000 rows; follow nextCursor for the rest.
|
||||
description: The maximum number of rows for raw and trace requests, or of
|
||||
series or rows for grouped scalar and time_series requests, keeping the
|
||||
top entries by order. When 0, scalar and time_series requests are unlimited,
|
||||
raw requests ordered by timestamp return a page of 10000 rows to continue
|
||||
with nextCursor, and other raw and trace requests return 100 rows.
|
||||
type: integer
|
||||
limitBy:
|
||||
allOf:
|
||||
@@ -10178,8 +10328,11 @@ components:
|
||||
operators and selectedQueryName refer to it.
|
||||
type: string
|
||||
offset:
|
||||
description: The number of rows to skip before returning results. Prefer
|
||||
cursor for raw requests.
|
||||
description: The number of rows for raw and trace requests, or of ordered
|
||||
series or rows for scalar and time_series requests, to skip before the
|
||||
page starts. Requires limit. On raw requests the skip counts from where
|
||||
a cursor resumes, and the skipped rows are still read, so prefer cursor
|
||||
for paging.
|
||||
type: integer
|
||||
order:
|
||||
description: The sort order of the result. Order by a group-by field, an
|
||||
@@ -10241,7 +10394,9 @@ components:
|
||||
the query they draw.
|
||||
cursor:
|
||||
description: The opaque nextCursor value from a previous raw result, used
|
||||
to fetch the next page.
|
||||
to fetch the next page. Honored only by raw requests whose order starts
|
||||
with timestamp, for logs followed by id in the same direction; every other
|
||||
request ignores it.
|
||||
type: string
|
||||
disabled:
|
||||
description: When true, the result is excluded from the response; the query
|
||||
@@ -10278,10 +10433,11 @@ components:
|
||||
are replaced by group-by label values.
|
||||
type: string
|
||||
limit:
|
||||
description: The maximum number of rows for raw requests, or of series or
|
||||
rows for grouped aggregations, keeping the top entries by order. When
|
||||
0, aggregations are unlimited while raw requests ordered by timestamp
|
||||
return a page of 10000 rows; follow nextCursor for the rest.
|
||||
description: The maximum number of rows for raw and trace requests, or of
|
||||
series or rows for grouped scalar and time_series requests, keeping the
|
||||
top entries by order. When 0, scalar and time_series requests are unlimited,
|
||||
raw requests ordered by timestamp return a page of 10000 rows to continue
|
||||
with nextCursor, and other raw and trace requests return 100 rows.
|
||||
type: integer
|
||||
limitBy:
|
||||
allOf:
|
||||
@@ -10293,8 +10449,11 @@ components:
|
||||
operators and selectedQueryName refer to it.
|
||||
type: string
|
||||
offset:
|
||||
description: The number of rows to skip before returning results. Prefer
|
||||
cursor for raw requests.
|
||||
description: The number of rows for raw and trace requests, or of ordered
|
||||
series or rows for scalar and time_series requests, to skip before the
|
||||
page starts. Requires limit. On raw requests the skip counts from where
|
||||
a cursor resumes, and the skipped rows are still read, so prefer cursor
|
||||
for paging.
|
||||
type: integer
|
||||
order:
|
||||
description: The sort order of the result. Order by a group-by field, an
|
||||
@@ -10356,7 +10515,9 @@ components:
|
||||
the query they draw.
|
||||
cursor:
|
||||
description: The opaque nextCursor value from a previous raw result, used
|
||||
to fetch the next page.
|
||||
to fetch the next page. Honored only by raw requests whose order starts
|
||||
with timestamp, for logs followed by id in the same direction; every other
|
||||
request ignores it.
|
||||
type: string
|
||||
disabled:
|
||||
description: When true, the result is excluded from the response; the query
|
||||
@@ -10393,10 +10554,11 @@ components:
|
||||
are replaced by group-by label values.
|
||||
type: string
|
||||
limit:
|
||||
description: The maximum number of rows for raw requests, or of series or
|
||||
rows for grouped aggregations, keeping the top entries by order. When
|
||||
0, aggregations are unlimited while raw requests ordered by timestamp
|
||||
return a page of 10000 rows; follow nextCursor for the rest.
|
||||
description: The maximum number of rows for raw and trace requests, or of
|
||||
series or rows for grouped scalar and time_series requests, keeping the
|
||||
top entries by order. When 0, scalar and time_series requests are unlimited,
|
||||
raw requests ordered by timestamp return a page of 10000 rows to continue
|
||||
with nextCursor, and other raw and trace requests return 100 rows.
|
||||
type: integer
|
||||
limitBy:
|
||||
allOf:
|
||||
@@ -10408,8 +10570,11 @@ components:
|
||||
operators and selectedQueryName refer to it.
|
||||
type: string
|
||||
offset:
|
||||
description: The number of rows to skip before returning results. Prefer
|
||||
cursor for raw requests.
|
||||
description: The number of rows for raw and trace requests, or of ordered
|
||||
series or rows for scalar and time_series requests, to skip before the
|
||||
page starts. Requires limit. On raw requests the skip counts from where
|
||||
a cursor resumes, and the skipped rows are still read, so prefer cursor
|
||||
for paging.
|
||||
type: integer
|
||||
order:
|
||||
description: The sort order of the result. Order by a group-by field, an
|
||||
@@ -10510,8 +10675,9 @@ components:
|
||||
description: The unique name of the trace operator query.
|
||||
type: string
|
||||
offset:
|
||||
description: The number of raw results to skip before returning. Aggregation
|
||||
requests ignore it.
|
||||
description: The number of raw or trace rows, or of ordered series or rows
|
||||
for time_series and scalar requests, to skip before the page starts. Requires
|
||||
limit.
|
||||
type: integer
|
||||
order:
|
||||
description: The sort order of the matched traces; only span_count and trace_duration
|
||||
@@ -10542,10 +10708,12 @@ components:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5TimeSeriesData'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5ScalarData'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5RawData'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5DistributionData'
|
||||
properties:
|
||||
results:
|
||||
description: 'One result per enabled query, typed by the request type: TimeSeriesData
|
||||
for time_series, ScalarData for scalar and RawData for raw requests.'
|
||||
for time_series and heatmap, ScalarData for scalar, RawData for raw and
|
||||
DistributionData for distribution requests.'
|
||||
items: {}
|
||||
nullable: true
|
||||
type: array
|
||||
@@ -10735,8 +10903,9 @@ components:
|
||||
type: object
|
||||
Querybuildertypesv5QueryRangeResponse:
|
||||
description: 'Response from the v5 query range endpoint. The data.results array
|
||||
contains typed results depending on the requestType: TimeSeriesData for time_series,
|
||||
ScalarData for scalar, or RawData for raw requests.'
|
||||
contains typed results depending on the requestType: TimeSeriesData for time_series
|
||||
and heatmap, ScalarData for scalar, RawData for raw, or DistributionData for
|
||||
distribution requests.'
|
||||
properties:
|
||||
data:
|
||||
allOf:
|
||||
@@ -10833,6 +11002,7 @@ components:
|
||||
- raw_stream
|
||||
- trace
|
||||
- heatmap
|
||||
- distribution
|
||||
type: string
|
||||
Querybuildertypesv5ScalarData:
|
||||
properties:
|
||||
@@ -10944,8 +11114,8 @@ components:
|
||||
format: double
|
||||
type: number
|
||||
values:
|
||||
description: The per-band counts for heatmap results, in the order of meta.buckets
|
||||
plus a trailing overflow count.
|
||||
description: The per-bucket counts for heatmap results, in the order of
|
||||
meta.buckets plus a trailing overflow count.
|
||||
items:
|
||||
format: double
|
||||
type: number
|
||||
@@ -24050,14 +24220,16 @@ paths:
|
||||
jira:
|
||||
description: Creates one Jira Cloud issue per alert group and reopens
|
||||
it if the alert fires again within `reopenDuration`. Authenticates
|
||||
with an Atlassian account email and API token.
|
||||
with an Atlassian account email and API token under `auth`.
|
||||
summary: Jira Cloud issues
|
||||
value:
|
||||
config:
|
||||
kind: jira
|
||||
spec:
|
||||
apiToken: XXXXXXXXXXXXXXXXXXXXXXXX
|
||||
email: oncall@example.com
|
||||
auth:
|
||||
apiToken: XXXXXXXXXXXXXXXXXXXXXXXX
|
||||
email: oncall@example.com
|
||||
deployment: cloud
|
||||
issueType: Bug
|
||||
labels:
|
||||
- signoz
|
||||
@@ -24067,6 +24239,23 @@ paths:
|
||||
site: https://example.atlassian.net
|
||||
displayName: Jira platform
|
||||
name: jira-platform
|
||||
jira-selfhosted:
|
||||
description: Creates issues on a Jira Data Center or Server site.
|
||||
Authenticates with a personal access token under `auth`, sent as
|
||||
a Bearer header.
|
||||
summary: Self-hosted Jira issues
|
||||
value:
|
||||
config:
|
||||
kind: jira
|
||||
spec:
|
||||
auth:
|
||||
pat: XXXXXXXXXXXXXXXXXXXXXXXX
|
||||
deployment: selfhosted
|
||||
issueType: Bug
|
||||
project: OPS
|
||||
site: https://jira.example.com
|
||||
displayName: Jira Data Center
|
||||
name: jira-datacenter
|
||||
jsmops:
|
||||
description: Creates JSM Ops alerts through an integration API key.
|
||||
JSM Ops speaks the Opsgenie alert API, so `priority` takes P1 to
|
||||
|
||||
@@ -45,8 +45,8 @@ const mockAliases = [
|
||||
},
|
||||
{
|
||||
// jest: not replaced, a test asserts on the toast it fires.
|
||||
find: /^@signozhq\/ui\/toast$/,
|
||||
replacement: `${srcPath}/storybook/mocks/toast.mock.tsx`,
|
||||
find: /^@signozhq\/ui\/sonner$/,
|
||||
replacement: `${srcPath}/storybook/mocks/sonner.mock.tsx`,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
# Popover migration analysis
|
||||
|
||||
The new `@signozhq/ui` Popover is click-only. This note maps the current app onto that API so a later migration does not break open state, focus, or the global `.ant-popover` overrides. It does not change any call site.
|
||||
|
||||
The component this describes lives in the design-system repo (`packages/ui/src/popover`). Storybook stories for it are `Playground`, `SaveViewForm`, `ControlledOpen`, `InsideDialog`, `MaxHeightScrolling`, and `AnchoredTypeahead`.
|
||||
|
||||
## The API to migrate onto
|
||||
|
||||
```tsx
|
||||
<Popover
|
||||
nativeButton
|
||||
side="bottom"
|
||||
align="start"
|
||||
trigger={<button type="button">Open</button>}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
{content}
|
||||
</Popover>
|
||||
```
|
||||
|
||||
| Today | New Popover |
|
||||
| --- | --- |
|
||||
| antd `children` (the trigger) | `trigger` (required) |
|
||||
| antd `content`, or Radix `<PopoverContent>` | `children` |
|
||||
| antd `trigger="click"` | the only mode; there is no `openOn` |
|
||||
| antd `trigger="hover"` | not this component; a Tooltip, or a click popover when the body is interactive |
|
||||
| `placement="bottomRight"` | `side="bottom"` `align="end"` |
|
||||
| `placement="bottomLeft"` | `side="bottom"` `align="start"` |
|
||||
| `open` / `onOpenChange` | kept; `onOpenChange` receives Base UI's reason and `details.cancel()` |
|
||||
| `defaultOpen` | removed; nothing in the app starts open |
|
||||
| `overlayClassName`, `overlayStyle`, `overlayInnerStyle`, `className` | rejected on the shell; size is `width` / `maxWidth` / `maxHeight` |
|
||||
| `getPopupContainer` | `container` |
|
||||
| `destroyTooltipOnHide` | the only behavior; the popup unmounts on close |
|
||||
| `arrow` / `showArrow` | rejected; the shell has no arrow |
|
||||
| Radix `PopoverAnchor` | `anchor`, and the trigger is still required |
|
||||
| Radix `withPortal={false}` | `container` (inside a Dialog the popup already portals into the panel) |
|
||||
| Radix `onInteractOutside` | `onOpenChange` + `details.cancel()` when `reason === 'outside-press'` |
|
||||
| `sideOffset` / `alignOffset` | not props; fixed at 4px and 0 |
|
||||
|
||||
`nativeButton` is required. `true` when `trigger` renders a `<button>` (including `Button`). `false` for anything else.
|
||||
|
||||
A node that renders nothing (`null`, `false`, `''`) renders the trigger alone, with no listeners and no portal.
|
||||
|
||||
## What is in the app today
|
||||
|
||||
Counted from `frontend/src` on this branch:
|
||||
|
||||
- **29 files** import `Popover` from `antd`.
|
||||
- **7 files** import `Popover` from `@signozhq/ui` (the Radix wrapper this rework replaces). Those seven break as soon as the package ships, because `children` stops being the trigger.
|
||||
- **18 SCSS files** override `.ant-popover*`.
|
||||
|
||||
antd `Dropdown` is already on `@signozhq/ui/dropdown`. antd `Tooltip` (on the order of 100 importers) is a separate migration.
|
||||
|
||||
### antd files
|
||||
|
||||
`src/components/CustomTimePicker/CustomTimePicker.tsx`
|
||||
`src/components/DownloadOptionsMenu/DownloadOptionsMenu.tsx`
|
||||
`src/components/ErrorPopover/ErrorPopover.tsx`
|
||||
`src/components/ExplorerCard/ExplorerCard.tsx`
|
||||
`src/components/HeaderRightSection/HeaderRightSection.tsx`
|
||||
`src/components/Logs/AddToQueryHOC.tsx`
|
||||
`src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx`
|
||||
`src/components/QueryBuilderV2/QueryV2/QueryAggregation/QueryAggregationSelect.tsx`
|
||||
`src/components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch.tsx`
|
||||
`src/components/QueryBuilderV2/QueryV2/TraceOperator/TraceOperatorEditor.tsx`
|
||||
`src/components/TableRenderer/LabelColumn.tsx`
|
||||
`src/components/WarningPopover/WarningPopover.tsx`
|
||||
`src/container/AlertHistory/AlertPopover/AlertPopover.tsx`
|
||||
`src/container/CreateAlertV2/EvaluationSettings/EvaluationSettings.tsx`
|
||||
`src/container/Home/Home.tsx`
|
||||
`src/container/MetricsExplorer/MetricDetails/AllAttributes.tsx`
|
||||
`src/container/MetricsExplorer/MetricDetails/AllAttributesValue.tsx`
|
||||
`src/container/MetricsExplorer/Summary/MetricNameSearch.tsx`
|
||||
`src/container/OptionsMenu/index.tsx`
|
||||
`src/container/TopNav/AutoRefreshV2/index.tsx`
|
||||
`src/pages/DashboardsListPage/components/ListHeader/ListHeader.tsx`
|
||||
`src/pages/MetricsApplication/ApDex/ApDexApplication.tsx`
|
||||
`src/pages/PublicDashboard/PublicDashboardView/PublicAutoRefresh/PublicAutoRefresh.tsx`
|
||||
`src/pages/TraceDetailsV3/SpanDetailsPanel/Events/components/AttributeWithExpandablePopover.tsx`
|
||||
`src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelStepPopover.tsx`
|
||||
`src/pages/TracesFunnels/components/FunnelsList/FunnelItemPopover.tsx`
|
||||
`src/pages/TracesFunnels/components/SearchBar/SearchBar.tsx`
|
||||
`src/periscope/components/ContextMenu/index.tsx`
|
||||
`src/periscope/components/SeeMore/SeeMore.tsx`
|
||||
|
||||
### Existing `@signozhq/ui` popovers
|
||||
|
||||
`src/components/ExportMenu/ExportMenu.tsx`
|
||||
`src/components/MarkdownEditor/MarkdownHelp.tsx`
|
||||
`src/components/TanStackTableView/TanStackHeaderRow.tsx`
|
||||
`src/container/AIAssistant/components/ChatInput/ChatInput.tsx`
|
||||
`src/container/ListAlertRules/components/ColumnSelector.tsx`
|
||||
`src/pages/DashboardPage/DashboardContainer/PanelEditor/ConfigPane/sections/ContextLinksSection/VariablesPopover.tsx`
|
||||
`src/pages/DashboardsListPage/components/ViewsRail/ViewNamePopover.tsx`
|
||||
|
||||
### SCSS that overrides `.ant-popover`
|
||||
|
||||
`src/components/DownloadOptionsMenu/DownloadOptionsMenu.styles.scss`
|
||||
`src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.styles.scss`
|
||||
`src/components/QueryBuilderV2/QueryV2/QueryAggregation/QueryAggregation.styles.scss`
|
||||
`src/components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch.styles.scss`
|
||||
`src/container/AlertHistory/AlertPopover/AlertPopover.styles.scss`
|
||||
`src/container/AlertHistory/Statistics/TopContributorsCard/TopContributorsCard.styles.scss`
|
||||
`src/container/CreateAlertV2/EvaluationSettings/styles.scss`
|
||||
`src/container/Home/Home.styles.scss`
|
||||
`src/container/MetricsExplorer/MetricDetails/MetricDetails.styles.scss`
|
||||
`src/container/MetricsExplorer/Summary/Summary.styles.scss`
|
||||
`src/container/TopNav/AutoRefreshV2/AutoRefreshV2.styles.scss`
|
||||
`src/container/TopNav/DateTimeSelectionV2/DateTimeSelectionV2.styles.scss`
|
||||
`src/container/WidgetCard/WidgetCard.styles.scss`
|
||||
`src/pages/DashboardsListPage/components/ListHeader/ListHeader.module.scss`
|
||||
`src/pages/TracesFunnels/TracesFunnels.styles.scss`
|
||||
`src/pages/TracesFunnels/components/FunnelsList/FunnelsList.styles.scss`
|
||||
`src/pages/TracesFunnels/components/SearchBar/SearchBar.styles.scss`
|
||||
`src/periscope/components/ContextMenu/styles.scss`
|
||||
|
||||
Three of these are unscoped and restyle every antd popover on the page, not just their own:
|
||||
|
||||
- `src/container/MetricsExplorer/Summary/Summary.styles.scss`
|
||||
- `src/container/CreateAlertV2/EvaluationSettings/styles.scss`
|
||||
- `src/container/AlertHistory/Statistics/TopContributorsCard/TopContributorsCard.styles.scss`
|
||||
|
||||
Scope or delete those three before moving any nearby popover, or a migrated popup and a leftover antd popup will share rules that only one of them understands.
|
||||
|
||||
Most of the other files repeat one recipe: padding 0, 4px radius, a 1px border, a translucent background, the arrow hidden. The new shell already has zero padding, `--radius-2`, and no arrow. Per-site size (fixed widths around 187–500px, max-width 600px, max-height 254px) becomes `width` / `maxWidth` / `maxHeight`. Do not re-apply the recipe as a `className`; the component rejects it.
|
||||
|
||||
## What should not become this Popover
|
||||
|
||||
**Hover descriptions.** `AddToQueryHOC`, `LabelColumn`, the three Query Builder validation popovers, `SeeMore`, and `AttributeWithExpandablePopover` open on hover. Those are Tooltips. `AttributeWithExpandablePopover` shows a value the user copies; if Tooltip cannot hold that, make it a click popover instead of keeping hover.
|
||||
|
||||
**WarningPopover** (imported from about 10 files) is hover today and contains docs links. A tooltip cannot host those links. Either the links move out and the summary becomes a Tooltip, or the wrapper becomes a click Popover with `maxWidth` around 600.
|
||||
|
||||
**Menus.** Funnel item and funnel step action lists, the sort lists in `ListHeader` and Traces Funnels `SearchBar`, `AutoRefreshV2` / `PublicAutoRefresh` interval lists, and `AlertPopover`'s two links are rows that close when one is picked. Those are Dropdowns. `AllAttributes` per-row actions are the same shape.
|
||||
|
||||
**ContextMenu** (`src/periscope/components/ContextMenu/index.tsx`, imported across the dashboard drilldown) opens at chart-click coordinates. There is no trigger element. The new Popover requires `trigger`, so this stays a context menu. Do not fold the phantom span, `position: fixed` overlay, `zIndex: 10000`, or `calculatePopoverPosition.ts` into `anchor`. `anchor` is for a popover that already has a trigger and should position against something else (the typeahead input row).
|
||||
|
||||
**Inspect charts.** `MetricsExplorer/Inspect` `GraphPopover` and `HoverPopover` are absolutely positioned cards, not antd Popover. Leave them until there is a context-menu or anchor design that matches them.
|
||||
|
||||
**PanelStatusPopover** is a Tooltip despite the name.
|
||||
|
||||
## What should become this Popover
|
||||
|
||||
Click panels whose content is a form, a picker, or a composed layout:
|
||||
|
||||
- `CustomTimePicker` — the most visible one. Controlled `open`, `container` for `getPopupContainer`, and it already unmounts on hide. It opens from both input focus and click; that logic stays in the caller, with `onOpenChange`.
|
||||
- `EvaluationSettings` / `EvaluationWindowPopover` — controlled, wide (`width` / `maxWidth`), and sitting on a global `.ant-popover` override. Migrate the override in the same change.
|
||||
- `HeaderRightSection` — three controlled forms (feedback, announcements, share).
|
||||
- `ExplorerCard` save-view form and `ViewNamePopover` — close only after the save succeeds, so `open` stays controlled. `ViewNamePopover` is already on the library and will not compile against the new props.
|
||||
- `ApDex` threshold form.
|
||||
- `Home` welcome checklist — drop the transparent-shell override; the shell is the component's.
|
||||
- `OptionsMenu`, `DownloadOptionsMenu`, `LogsFormatOptionsMenu` — click panels. `LogsFormatOptionsMenu` nests a submenu; the inner popup must be portalled, and the outer `onOpenChange` must `cancel()` an outside press that lands in the inner popup.
|
||||
- `MetricNameSearch` — controlled search panel. Scope the global padding override in `Summary.styles.scss` in the same change.
|
||||
- `AllAttributesValue` — one popover open at a time in a list. That is controlled `open` keyed by row, which the API still allows.
|
||||
- `ChatInput` context picker — already on the library; `side="top"` `align="end"` plus `width`.
|
||||
- `VariablesPopover` — already on the library. Keep `anchor` on the input row, `initialFocus={false}` so the caret stays, and `details.cancel()` when the outside press is the anchor. `withPortal={false}` becomes the default dialog `container`, not an unportalled popup.
|
||||
- `ColumnSelector`, `MarkdownHelp`, `TanStackHeaderRow`, `ExportMenu` — Radix `asChild` trigger plus content. The trigger moves to the `trigger` prop, content moves to `children`, and `className` on the shell becomes `width` / `maxWidth`.
|
||||
|
||||
`ErrorPopover` is a thin wrapper. Re-point it at the new props and its consumer follows.
|
||||
|
||||
## Order
|
||||
|
||||
Before any change: screenshot every current popover, in both themes, so each call site's pattern is on record and a component swap is reviewable against it. Flag the misusages to design as they turn up — links inside hover surfaces above all. Swapping the component during migration is expected where the old call site was on the wrong one; antd call sites stay untouched until their turn.
|
||||
|
||||
1. Scope the three global `.ant-popover` files so later migrations cannot restyle leftover antd popovers.
|
||||
2. Move the seven existing `@signozhq/ui` call sites first. They break at the package bump; the 29 antd files do not, until each one is switched.
|
||||
3. Migrate shared wrappers that are actually popovers (`ErrorPopover`, then `WarningPopover` once the links decision is made). Leave `ContextMenu` on antd until a context menu exists.
|
||||
4. Migrate click forms (`CustomTimePicker`, evaluation window, header menus, save-view, ApDex).
|
||||
5. Send hover descriptions to Tooltip and row menus to Dropdown, instead of forcing them through Popover.
|
||||
6. Delete the matching `.ant-popover` rules in the same change as each call site, so a half-migrated page is not styled twice.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Controlled `open` is load-bearing.** Save forms close from inside the popup only when the mutation succeeds. A list keeps one popover open at a time. Dropping `open` and driving everything from `onOpenChange` alone cannot express either. Keep the controlled prop at those sites.
|
||||
- **Focus moves in.** Click popovers focus the first tabbable child. Typeaheads (`VariablesPopover`, and any filter that must keep the caret) need `initialFocus={false}`.
|
||||
- **Outside press and anchors.** A press on the anchor looks outside. Call `details.cancel()` or the typeahead closes when the user clicks the input it is anchored to.
|
||||
- **Dialogs.** Inside a Dialog or Drawer the popup portals into the panel so the focus trap can reach it. Do not set `container={document.body}` there.
|
||||
- **Z-index.** The shell starts at `--popover-z-index` (50) and lifts above the layer its trigger sits in. A Tooltip inside a row must still paint above the popup. The app ladder today is tooltip 1070, dropdown 1050, popover 1030, modal 1000. Check a truncated row tooltip inside a popover after the first real migration.
|
||||
- **Scroll locking.** Chart popovers measure the wrong box when a modal has locked scroll. This popover does not lock scroll (`modal` is rejected). Re-check Infrastructure Monitoring charts if a surrounding modal is open.
|
||||
- **`nativeButton` mismatches.** Base UI logs an error when the flag does not match the element. Every non-button trigger (a badge, a custom row, an input) needs `nativeButton={false}`.
|
||||
|
||||
## `--popover-foreground` audit
|
||||
|
||||
The shell reads `--popover-background`, `--popover-foreground`, `--popover-border`, and `--popover-shadow`. Those semantic values belong to the token package, not to call sites.
|
||||
|
||||
Two legacy shadcn variables predate those hooks:
|
||||
|
||||
- `--popover-foreground` is the foreground hook the shell uses. It should stay pointed at the same foreground as the rest of the light and dark themes (`--l1-foreground` today).
|
||||
- `--popover` (no suffix) is the old background. `--calendar-select-background` still reads it. Do not repoint `--popover` at `--popover-background` or at `surface-3` until the calendar token is audited, or date selection changes color with the popover shell.
|
||||
|
||||
Call sites that pass antd's `color` (a gradient string on `AlertPopover`, a raw token on `SeeMore`) do not map to a prop. The shell color comes from the tokens. Content that needs its own background paints it inside `children`.
|
||||
@@ -53,9 +53,9 @@
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@sentry/react": "10.57.0",
|
||||
"@sentry/vite-plugin": "5.3.0",
|
||||
"@signozhq/design-tokens": "2.3.0",
|
||||
"@signozhq/design-tokens": "2.2.1",
|
||||
"@signozhq/icons": "0.4.0",
|
||||
"@signozhq/ui": "0.3.0",
|
||||
"@signozhq/ui": "0.2.0",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
|
||||
450
frontend/pnpm-lock.yaml
generated
450
frontend/pnpm-lock.yaml
generated
@@ -74,14 +74,14 @@ importers:
|
||||
specifier: 5.3.0
|
||||
version: 5.3.0
|
||||
'@signozhq/design-tokens':
|
||||
specifier: 2.3.0
|
||||
version: 2.3.0
|
||||
specifier: 2.2.1
|
||||
version: 2.2.1
|
||||
'@signozhq/icons':
|
||||
specifier: 0.4.0
|
||||
version: 0.4.0
|
||||
'@signozhq/ui':
|
||||
specifier: 0.3.0
|
||||
version: 0.3.0(@date-fns/tz@1.4.1)(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(date-fns@4.1.0)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)
|
||||
specifier: 0.2.0
|
||||
version: 0.2.0(@date-fns/tz@1.4.1)(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(date-fns@4.1.0)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)
|
||||
'@tanstack/react-table':
|
||||
specifier: 8.21.3
|
||||
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -2909,6 +2909,9 @@ packages:
|
||||
'@posthog/core@1.6.0':
|
||||
resolution: {integrity: sha512-Tbh8UACwbb7jFdDC7wwXHtfNzO+4wKh3VbyMHmp2UBe6w1jliJixexTJNfkqdGZm+ht3M10mcKvGGPnoZ2zLBg==}
|
||||
|
||||
'@radix-ui/number@1.1.3':
|
||||
resolution: {integrity: sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==}
|
||||
|
||||
'@radix-ui/primitive@1.1.7':
|
||||
resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==}
|
||||
|
||||
@@ -2925,6 +2928,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-collection@1.1.15':
|
||||
resolution: {integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.5':
|
||||
resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==}
|
||||
peerDependencies:
|
||||
@@ -2956,6 +2972,15 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-direction@1.1.4':
|
||||
resolution: {integrity: sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-dismissable-layer@1.1.19':
|
||||
resolution: {integrity: sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==}
|
||||
peerDependencies:
|
||||
@@ -3070,6 +3095,71 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-progress@1.1.16':
|
||||
resolution: {integrity: sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-radio-group@1.4.7':
|
||||
resolution: {integrity: sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-roving-focus@1.1.19':
|
||||
resolution: {integrity: sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-select@2.3.7':
|
||||
resolution: {integrity: sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slider@1.4.7':
|
||||
resolution: {integrity: sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slot@1.3.3':
|
||||
resolution: {integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==}
|
||||
peerDependencies:
|
||||
@@ -3079,6 +3169,19 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-tabs@1.1.21':
|
||||
resolution: {integrity: sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.4':
|
||||
resolution: {integrity: sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==}
|
||||
peerDependencies:
|
||||
@@ -3106,6 +3209,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-is-hydrated@0.1.3':
|
||||
resolution: {integrity: sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.1.4':
|
||||
resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==}
|
||||
peerDependencies:
|
||||
@@ -3115,6 +3227,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-previous@1.1.4':
|
||||
resolution: {integrity: sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.4':
|
||||
resolution: {integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==}
|
||||
peerDependencies:
|
||||
@@ -3133,6 +3254,19 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-visually-hidden@1.2.11':
|
||||
resolution: {integrity: sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/rect@1.1.3':
|
||||
resolution: {integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==}
|
||||
|
||||
@@ -3451,14 +3585,14 @@ packages:
|
||||
'@sideway/pinpoint@2.0.0':
|
||||
resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
|
||||
|
||||
'@signozhq/design-tokens@2.3.0':
|
||||
resolution: {integrity: sha512-gVTPc2uJwBsTF9VMzPghgsnv6eioK8oxzES1CLDxedxpcfwbWltuzxVPwhPuoC54E2vItDVZhR9RFI/JZ50ldw==}
|
||||
'@signozhq/design-tokens@2.2.1':
|
||||
resolution: {integrity: sha512-ulf6lhWUP6qxqsyemQySUMfOIKRvefvZ5rBRv+wlhOpPfYPk3uatPmPkKEhAjISpX8f9g1Gg8wgRZxUCysf20Q==}
|
||||
|
||||
'@signozhq/icons@0.4.0':
|
||||
resolution: {integrity: sha512-QctNvNPlQlsefFdIVmSy/P/7a1IcQ3IryKP0KPMqtoqew3q89FGUmFa5IfEJ7Z3CHhtG1zaXwBKqP72QkLyA3g==}
|
||||
|
||||
'@signozhq/ui@0.3.0':
|
||||
resolution: {integrity: sha512-Khzr3/YC/QACwBi1zfQwt+h88iyuIu9hm5hkcIkuCTteD9P+r13US/NN4kkbJe8IHyZXCoDkoIpmQxzbrA3/SA==}
|
||||
'@signozhq/ui@0.2.0':
|
||||
resolution: {integrity: sha512-j20ldiOkR150ON0BCDhlq6fXGoPN++7w6e4t2HtM2XMM4buJO7aHzPzNKvDRZ2UUf/LGPULPAW8w6sg6gUVHyw==}
|
||||
peerDependencies:
|
||||
'@signozhq/icons': 0.3.0
|
||||
react: ^18.2.0
|
||||
@@ -4791,6 +4925,12 @@ packages:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
cmdk@1.1.1:
|
||||
resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
co@4.6.0:
|
||||
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
||||
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
||||
@@ -7319,6 +7459,12 @@ packages:
|
||||
neo-async@2.6.2:
|
||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||
|
||||
next-themes@0.4.6:
|
||||
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
node-addon-api@7.1.1:
|
||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||
|
||||
@@ -8287,8 +8433,8 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react-resizable-panels@4.12.2:
|
||||
resolution: {integrity: sha512-NwY5LCo4WrxVvDh0xoMML6EMLPONP/8ckKcIdpnojxexoatZdjLiRqLJQjQK5CPkd4SYiB/2M5BVrjZBQtOO7Q==}
|
||||
react-resizable-panels@4.12.3:
|
||||
resolution: {integrity: sha512-GHMJWnDXui/3RX4bT+cgBP+N3N2nkwcoZATr/2xLFpqQQe7TlBrE0cGM0dUa9ceT7A90tUrSRsiqgRG+g0/2AA==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
@@ -8788,6 +8934,12 @@ packages:
|
||||
resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
sonner@2.0.7:
|
||||
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
|
||||
sort-asc@0.1.0:
|
||||
resolution: {integrity: sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -9798,7 +9950,7 @@ snapshots:
|
||||
|
||||
'@ant-design/cssinjs@1.17.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
'@emotion/hash': 0.8.0
|
||||
'@emotion/unitless': 0.7.5
|
||||
classnames: 2.3.2
|
||||
@@ -9814,7 +9966,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@ant-design/colors': 7.0.0
|
||||
'@ant-design/icons-svg': 4.3.1
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
@@ -9822,7 +9974,7 @@ snapshots:
|
||||
|
||||
'@ant-design/react-slick@1.0.2(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
json2mq: 0.2.0
|
||||
react: 18.2.0
|
||||
@@ -12346,6 +12498,8 @@ snapshots:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
|
||||
'@radix-ui/number@1.1.3': {}
|
||||
|
||||
'@radix-ui/primitive@1.1.7': {}
|
||||
|
||||
'@radix-ui/react-arrow@1.1.15(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
@@ -12357,6 +12511,18 @@ snapshots:
|
||||
'@types/react': 18.0.26
|
||||
'@types/react-dom': 18.0.10
|
||||
|
||||
'@radix-ui/react-collection@1.1.15(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-slot': 1.3.3(@types/react@18.0.26)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
'@types/react-dom': 18.0.10
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.5(@types/react@18.0.26)(react@18.2.0)':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
@@ -12392,6 +12558,12 @@ snapshots:
|
||||
'@types/react': 18.0.26
|
||||
'@types/react-dom': 18.0.10
|
||||
|
||||
'@radix-ui/react-direction@1.1.4(@types/react@18.0.26)(react@18.2.0)':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
|
||||
'@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.7
|
||||
@@ -12502,6 +12674,101 @@ snapshots:
|
||||
'@types/react': 18.0.26
|
||||
'@types/react-dom': 18.0.10
|
||||
|
||||
'@radix-ui/react-progress@1.1.16(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
'@types/react-dom': 18.0.10
|
||||
|
||||
'@radix-ui/react-radio-group@1.4.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.7
|
||||
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-direction': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-presence': 1.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-use-size': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
'@types/react-dom': 18.0.10
|
||||
|
||||
'@radix-ui/react-roving-focus@1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.7
|
||||
'@radix-ui/react-collection': 1.1.15(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-direction': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-id': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
'@types/react-dom': 18.0.10
|
||||
|
||||
'@radix-ui/react-select@2.3.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/number': 1.1.3
|
||||
'@radix-ui/primitive': 1.1.7
|
||||
'@radix-ui/react-collection': 1.1.15(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-direction': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-focus-guards': 1.1.6(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-id': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-popper': 1.3.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-portal': 1.1.17(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-presence': 1.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-slot': 1.3.3(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-use-previous': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
aria-hidden: 1.2.6
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-remove-scroll: 2.7.2(@types/react@18.0.26)(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
'@types/react-dom': 18.0.10
|
||||
|
||||
'@radix-ui/react-slider@1.4.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/number': 1.1.3
|
||||
'@radix-ui/primitive': 1.1.7
|
||||
'@radix-ui/react-collection': 1.1.15(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-direction': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-use-previous': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-use-size': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
'@types/react-dom': 18.0.10
|
||||
|
||||
'@radix-ui/react-slot@1.3.3(@types/react@18.0.26)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
|
||||
@@ -12509,6 +12776,22 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
|
||||
'@radix-ui/react-tabs@1.1.21(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.7
|
||||
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-direction': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-id': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-presence': 1.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
'@types/react-dom': 18.0.10
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.4(@types/react@18.0.26)(react@18.2.0)':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
@@ -12531,12 +12814,24 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
|
||||
'@radix-ui/react-use-is-hydrated@0.1.3(@types/react@18.0.26)(react@18.2.0)':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.1.4(@types/react@18.0.26)(react@18.2.0)':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
|
||||
'@radix-ui/react-use-previous@1.1.4(@types/react@18.0.26)(react@18.2.0)':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.4(@types/react@18.0.26)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/rect': 1.1.3
|
||||
@@ -12551,11 +12846,20 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
|
||||
'@radix-ui/react-visually-hidden@1.2.11(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
'@types/react-dom': 18.0.10
|
||||
|
||||
'@radix-ui/rect@1.1.3': {}
|
||||
|
||||
'@rc-component/color-picker@1.4.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
'@ctrl/tinycolor': 3.6.1
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -12575,7 +12879,7 @@ snapshots:
|
||||
|
||||
'@rc-component/mutate-observer@1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
@@ -12591,7 +12895,7 @@ snapshots:
|
||||
|
||||
'@rc-component/tour@1.10.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
'@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@rc-component/trigger': 1.18.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
classnames: 2.3.2
|
||||
@@ -12601,7 +12905,7 @@ snapshots:
|
||||
|
||||
'@rc-component/trigger@1.18.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
'@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
classnames: 2.3.2
|
||||
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -12846,33 +13150,41 @@ snapshots:
|
||||
|
||||
'@sideway/pinpoint@2.0.0': {}
|
||||
|
||||
'@signozhq/design-tokens@2.3.0': {}
|
||||
'@signozhq/design-tokens@2.2.1': {}
|
||||
|
||||
'@signozhq/icons@0.4.0': {}
|
||||
|
||||
'@signozhq/ui@0.3.0(@date-fns/tz@1.4.1)(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(date-fns@4.1.0)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)':
|
||||
'@signozhq/ui@0.2.0(@date-fns/tz@1.4.1)(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(date-fns@4.1.0)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@base-ui/react': 1.7.0(@date-fns/tz@1.4.1)(@types/react@18.0.26)(date-fns@4.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@chenglou/pretext': 0.0.8
|
||||
'@radix-ui/react-dialog': 1.1.23(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-icons': 1.3.2(react@18.2.0)
|
||||
'@radix-ui/react-popover': 1.1.23(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-progress': 1.1.16(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-radio-group': 1.4.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-select': 2.3.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-slider': 1.4.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-slot': 1.3.3(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-tabs': 1.1.21(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@signozhq/icons': 0.4.0
|
||||
'@tanstack/react-table': 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@tanstack/react-virtual': 3.14.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@types/lodash-es': 4.17.12
|
||||
clsx: 2.1.1
|
||||
cmdk: 1.1.1(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
copy-text-to-clipboard: 3.2.2
|
||||
dayjs: 1.11.23
|
||||
lodash-es: 4.18.1
|
||||
motion: 11.18.2(@emotion/is-prop-valid@1.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next-themes: 0.4.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
nuqs: 2.9.6(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-compiler-runtime: 1.0.0(react@18.2.0)
|
||||
react-day-picker: 9.9.0(react@18.2.0)
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-resizable-panels: 4.12.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react-resizable-panels: 4.12.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
sonner: 2.0.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
transitivePeerDependencies:
|
||||
- '@date-fns/tz'
|
||||
- '@emotion/is-prop-valid'
|
||||
@@ -14122,7 +14434,7 @@ snapshots:
|
||||
|
||||
better-xlsx@0.7.6:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
jszip: 3.10.1
|
||||
kind-of: 6.0.3
|
||||
|
||||
@@ -14159,7 +14471,7 @@ snapshots:
|
||||
|
||||
broadcast-channel@3.7.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
detect-node: 2.1.0
|
||||
js-sha3: 0.8.0
|
||||
microseconds: 0.2.0
|
||||
@@ -14368,6 +14680,18 @@ snapshots:
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
cmdk@1.1.1(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-dialog': 1.1.23(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-id': 1.1.4(@types/react@18.0.26)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
|
||||
co@4.6.0: {}
|
||||
|
||||
codemirror@6.0.1:
|
||||
@@ -16916,7 +17240,7 @@ snapshots:
|
||||
|
||||
match-sorter@6.3.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
remove-accents: 0.4.2
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
@@ -17426,6 +17750,11 @@ snapshots:
|
||||
|
||||
neo-async@2.6.2: {}
|
||||
|
||||
next-themes@0.4.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
node-addon-api@7.1.1:
|
||||
optional: true
|
||||
|
||||
@@ -18049,7 +18378,7 @@ snapshots:
|
||||
|
||||
rc-cascader@3.20.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
array-tree-filter: 2.1.0
|
||||
classnames: 2.3.2
|
||||
rc-select: 14.10.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18060,7 +18389,7 @@ snapshots:
|
||||
|
||||
rc-checkbox@3.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
@@ -18068,7 +18397,7 @@ snapshots:
|
||||
|
||||
rc-collapse@3.7.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18077,7 +18406,7 @@ snapshots:
|
||||
|
||||
rc-dialog@9.3.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
'@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
classnames: 2.3.2
|
||||
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18087,7 +18416,7 @@ snapshots:
|
||||
|
||||
rc-drawer@6.5.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
'@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
classnames: 2.3.2
|
||||
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18097,7 +18426,7 @@ snapshots:
|
||||
|
||||
rc-dropdown@4.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
'@rc-component/trigger': 1.18.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18106,7 +18435,7 @@ snapshots:
|
||||
|
||||
rc-field-form@1.40.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
async-validator: 4.2.5
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
@@ -18114,7 +18443,7 @@ snapshots:
|
||||
|
||||
rc-image@7.3.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
'@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
classnames: 2.3.2
|
||||
rc-dialog: 9.3.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18125,7 +18454,7 @@ snapshots:
|
||||
|
||||
rc-input-number@8.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
'@rc-component/mini-decimal': 1.0.1
|
||||
classnames: 2.3.2
|
||||
rc-input: 1.3.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18135,7 +18464,7 @@ snapshots:
|
||||
|
||||
rc-input@1.3.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
@@ -18143,7 +18472,7 @@ snapshots:
|
||||
|
||||
rc-mentions@2.9.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
'@rc-component/trigger': 1.18.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
classnames: 2.3.2
|
||||
rc-input: 1.3.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18155,7 +18484,7 @@ snapshots:
|
||||
|
||||
rc-menu@9.12.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
'@rc-component/trigger': 1.18.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
classnames: 2.3.2
|
||||
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18166,7 +18495,7 @@ snapshots:
|
||||
|
||||
rc-motion@2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
@@ -18174,7 +18503,7 @@ snapshots:
|
||||
|
||||
rc-notification@5.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18183,7 +18512,7 @@ snapshots:
|
||||
|
||||
rc-overflow@1.3.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18192,7 +18521,7 @@ snapshots:
|
||||
|
||||
rc-pagination@3.7.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
@@ -18200,7 +18529,7 @@ snapshots:
|
||||
|
||||
rc-picker@3.14.6(date-fns@4.1.0)(dayjs@1.11.20)(moment@2.30.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
'@rc-component/trigger': 1.18.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18213,7 +18542,7 @@ snapshots:
|
||||
|
||||
rc-progress@3.5.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
@@ -18221,7 +18550,7 @@ snapshots:
|
||||
|
||||
rc-rate@2.12.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
@@ -18229,7 +18558,7 @@ snapshots:
|
||||
|
||||
rc-resize-observer@1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
@@ -18238,7 +18567,7 @@ snapshots:
|
||||
|
||||
rc-segmented@2.2.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18259,7 +18588,7 @@ snapshots:
|
||||
|
||||
rc-slider@10.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
@@ -18267,7 +18596,7 @@ snapshots:
|
||||
|
||||
rc-steps@6.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
@@ -18275,7 +18604,7 @@ snapshots:
|
||||
|
||||
rc-switch@4.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
@@ -18283,7 +18612,7 @@ snapshots:
|
||||
|
||||
rc-table@7.35.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
'@rc-component/context': 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
classnames: 2.3.2
|
||||
rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18294,7 +18623,7 @@ snapshots:
|
||||
|
||||
rc-tabs@12.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-dropdown: 4.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
rc-menu: 9.12.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18306,7 +18635,7 @@ snapshots:
|
||||
|
||||
rc-textarea@1.5.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-input: 1.3.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18316,7 +18645,7 @@ snapshots:
|
||||
|
||||
rc-tooltip@6.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
'@rc-component/trigger': 1.18.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
classnames: 2.3.2
|
||||
react: 18.2.0
|
||||
@@ -18324,7 +18653,7 @@ snapshots:
|
||||
|
||||
rc-tree-select@5.15.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-select: 14.10.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
rc-tree: 5.8.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18334,7 +18663,7 @@ snapshots:
|
||||
|
||||
rc-tree@5.8.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18344,7 +18673,7 @@ snapshots:
|
||||
|
||||
rc-upload@4.3.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
@@ -18352,14 +18681,14 @@ snapshots:
|
||||
|
||||
rc-util@5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-is: 18.3.1
|
||||
|
||||
rc-virtual-list@3.11.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
classnames: 2.3.2
|
||||
rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -18585,7 +18914,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
|
||||
react-resizable-panels@4.12.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
react-resizable-panels@4.12.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
@@ -18636,7 +18965,7 @@ snapshots:
|
||||
|
||||
react-router@5.3.4(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
history: 4.10.1
|
||||
hoist-non-react-statics: 3.3.2
|
||||
loose-envify: 1.4.0
|
||||
@@ -19176,6 +19505,11 @@ snapshots:
|
||||
ansi-styles: 6.2.3
|
||||
is-fullwidth-code-point: 5.1.0
|
||||
|
||||
sonner@2.0.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
sort-asc@0.1.0: {}
|
||||
|
||||
sort-desc@0.1.1: {}
|
||||
|
||||
@@ -18,15 +18,25 @@
|
||||
"tooltip_email_to": "Enter email addresses separated by commas.",
|
||||
"tooltip_ms_teams_url": "The URL of the Microsoft Teams [webhook](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498) to send alerts to. Learn more about Microsoft Teams integration in the docs [here](https://signoz.io/docs/alerts-management/notification-channel/ms-teams/).",
|
||||
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to.",
|
||||
"field_jira_deployment": "Jira deployment",
|
||||
"option_jira_deployment_cloud": "Jira Cloud",
|
||||
"option_jira_deployment_selfhosted": "Self-hosted Jira",
|
||||
"jira_selfhosted_tip": "Works with Jira Data Center 8.14 and newer. Create the personal access token from a dedicated service user so issues aren't reported under a personal name.",
|
||||
"jira_selfhosted_tip_link": "Learn how",
|
||||
"field_jira_site": "Site URL",
|
||||
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net, or a proxy in front of it.",
|
||||
"jira_required_fields": "Site URL, email, API token, project and issue type are required",
|
||||
"tooltip_jira_site_selfhosted": "Your self-hosted Jira base URL, e.g. https://jira.your-company.com, or a proxy in front of it.",
|
||||
"jira_required_fields": "Site URL, project and issue type are required",
|
||||
"jira_cloud_credentials_required": "Email and API token are required for Jira Cloud",
|
||||
"jira_pat_required": "A personal access token is required for self-hosted Jira",
|
||||
"jira_service_account_tip": "Recommended: use a Jira service account so alerts aren't reported under a personal name and the channel keeps working when someone leaves.",
|
||||
"jira_service_account_tip_link": "Learn how",
|
||||
"field_jira_email": "Email",
|
||||
"help_jira_email": "The Atlassian account email used for authentication.",
|
||||
"field_jira_api_token": "API token",
|
||||
"help_jira_api_token": "Create one at id.atlassian.com under Security → API tokens.",
|
||||
"field_jira_pat": "Personal access token",
|
||||
"help_jira_pat": "Create one in Jira under your profile, Personal Access Tokens. Sent as a Bearer header.",
|
||||
"field_jira_project": "Project key",
|
||||
"field_jira_issue_type": "Issue type",
|
||||
"help_jira_issue_type": "An issue type that exists in the project, e.g. Task, Bug or Incident.",
|
||||
@@ -34,6 +44,7 @@
|
||||
"help_jira_summary": "Template for the Jira issue title.",
|
||||
"field_jira_description": "Description",
|
||||
"help_jira_description": "Template for the issue description. Rendered as rich text with a status panel and links back to SigNoz.",
|
||||
"help_jira_description_selfhosted": "Template for the issue description. Rendered as Jira wiki markup with a status panel and links back to SigNoz.",
|
||||
"jira_advanced_section": "Advanced Options",
|
||||
"field_jira_priority": "Priority",
|
||||
"placeholder_jira_priority": "Leave empty to use the project default",
|
||||
|
||||
@@ -26,15 +26,25 @@
|
||||
"tooltip_email_to": "Enter email addresses separated by commas.",
|
||||
"tooltip_ms_teams_url": "The URL of the Microsoft Teams [webhook](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498) to send alerts to. Learn more about Microsoft Teams integration in the docs [here](https://signoz.io/docs/alerts-management/notification-channel/ms-teams/).",
|
||||
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to.",
|
||||
"field_jira_deployment": "Jira deployment",
|
||||
"option_jira_deployment_cloud": "Jira Cloud",
|
||||
"option_jira_deployment_selfhosted": "Self-hosted Jira",
|
||||
"jira_selfhosted_tip": "Works with Jira Data Center 8.14 and newer. Create the personal access token from a dedicated service user so issues aren't reported under a personal name.",
|
||||
"jira_selfhosted_tip_link": "Learn how",
|
||||
"field_jira_site": "Site URL",
|
||||
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net, or a proxy in front of it.",
|
||||
"jira_required_fields": "Site URL, email, API token, project and issue type are required",
|
||||
"tooltip_jira_site_selfhosted": "Your self-hosted Jira base URL, e.g. https://jira.your-company.com, or a proxy in front of it.",
|
||||
"jira_required_fields": "Site URL, project and issue type are required",
|
||||
"jira_cloud_credentials_required": "Email and API token are required for Jira Cloud",
|
||||
"jira_pat_required": "A personal access token is required for self-hosted Jira",
|
||||
"jira_service_account_tip": "Recommended: use a Jira service account so alerts aren't reported under a personal name and the channel keeps working when someone leaves.",
|
||||
"jira_service_account_tip_link": "Learn how",
|
||||
"field_jira_email": "Email",
|
||||
"help_jira_email": "The Atlassian account email used for authentication.",
|
||||
"field_jira_api_token": "API token",
|
||||
"help_jira_api_token": "Create one at id.atlassian.com under Security → API tokens.",
|
||||
"field_jira_pat": "Personal access token",
|
||||
"help_jira_pat": "Create one in Jira under your profile, Personal Access Tokens. Sent as a Bearer header.",
|
||||
"field_jira_project": "Project key",
|
||||
"field_jira_issue_type": "Issue type",
|
||||
"help_jira_issue_type": "An issue type that exists in the project, e.g. Task, Bug or Incident.",
|
||||
@@ -42,6 +52,7 @@
|
||||
"help_jira_summary": "Template for the Jira issue title.",
|
||||
"field_jira_description": "Description",
|
||||
"help_jira_description": "Template for the issue description. Rendered as rich text with a status panel and links back to SigNoz.",
|
||||
"help_jira_description_selfhosted": "Template for the issue description. Rendered as Jira wiki markup with a status panel and links back to SigNoz.",
|
||||
"jira_advanced_section": "Advanced Options",
|
||||
"field_jira_priority": "Priority",
|
||||
"placeholder_jira_priority": "Leave empty to use the project default",
|
||||
|
||||
@@ -829,6 +829,37 @@ export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTy
|
||||
export enum AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJiraConfigDTOKind {
|
||||
jira = 'jira',
|
||||
}
|
||||
export interface AlertmanagertypesChannelJiraCloudAuthDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format password
|
||||
* @description The Atlassian API token paired with email. A secret; returned by create, update and fetch by ID, never by list.
|
||||
*/
|
||||
apiToken: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description The Atlassian account email used for basic auth.
|
||||
*/
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesChannelJiraSelfHostedAuthDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format password
|
||||
* @description The Jira personal access token, sent as a Bearer header. A secret; returned by create, update and fetch by ID, never by list.
|
||||
*/
|
||||
pat: string;
|
||||
}
|
||||
|
||||
export type AlertmanagertypesChannelJiraAuthDTO =
|
||||
| AlertmanagertypesChannelJiraCloudAuthDTO
|
||||
| AlertmanagertypesChannelJiraSelfHostedAuthDTO;
|
||||
|
||||
export enum AlertmanagertypesJiraDeploymentDTO {
|
||||
cloud = 'cloud',
|
||||
selfhosted = 'selfhosted',
|
||||
}
|
||||
/**
|
||||
* Extra issue fields keyed by Jira field ID. Stored, but not currently included in the issue Jira receives.
|
||||
*/
|
||||
@@ -844,26 +875,23 @@ export type AlertmanagertypesChannelJiraConfigDTOCustomFields = Alertmanagertype
|
||||
|
||||
export interface AlertmanagertypesChannelJiraConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format password
|
||||
* @description The Atlassian API token paired with email. A secret; returned by create, update and fetch by ID, never by list.
|
||||
* @description The credentials: email and apiToken when deployment is cloud, pat when deployment is selfhosted.
|
||||
*/
|
||||
apiToken: string;
|
||||
auth: AlertmanagertypesChannelJiraAuthDTO;
|
||||
/**
|
||||
* @type object,null
|
||||
* @description Extra issue fields keyed by Jira field ID. Stored, but not currently included in the issue Jira receives.
|
||||
*/
|
||||
customFields?: AlertmanagertypesChannelJiraConfigDTOCustomFields;
|
||||
/**
|
||||
* @description The Jira edition: cloud for an Atlassian-hosted site, selfhosted for Jira Server or Data Center.
|
||||
*/
|
||||
deployment: AlertmanagertypesJiraDeploymentDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @description Alertmanager template for the issue description, rendered as markdown; defaults to the SigNoz Jira default.
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description The Atlassian account email used for basic auth.
|
||||
*/
|
||||
email: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description The issue type name, such as Bug.
|
||||
@@ -906,7 +934,7 @@ export interface AlertmanagertypesChannelJiraConfigDTO {
|
||||
sendResolved?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
* @description The base URL of the Jira Cloud site, for example https://example.atlassian.net. Only Jira Cloud is supported.
|
||||
* @description The base URL of the Jira site, for example https://example.atlassian.net or https://jira.example.com.
|
||||
*/
|
||||
site: string;
|
||||
/**
|
||||
@@ -1716,13 +1744,17 @@ export interface AlertmanagertypesJiraReceiverConfigDTO {
|
||||
* @description Extra issue fields keyed by Jira field ID. Stored, but not currently included in the issue Jira receives.
|
||||
*/
|
||||
custom_fields?: AlertmanagertypesJiraReceiverConfigDTOCustomFields;
|
||||
/**
|
||||
* @description The Jira edition, cloud or selfhosted; defaults to cloud.
|
||||
*/
|
||||
deployment?: AlertmanagertypesJiraDeploymentDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @description Alertmanager template for the issue description.
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @description HTTP client settings; basic auth carries the account email and API token.
|
||||
* @description HTTP client settings; basic auth carries the account email and API token on cloud, authorization carries the Bearer personal access token on self-hosted.
|
||||
*/
|
||||
http_config?: ConfigHTTPClientConfigDTO;
|
||||
/**
|
||||
@@ -1765,7 +1797,7 @@ export interface AlertmanagertypesJiraReceiverConfigDTO {
|
||||
send_resolved?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
* @description The Jira Cloud base URL.
|
||||
* @description The Jira base URL.
|
||||
*/
|
||||
site?: string;
|
||||
/**
|
||||
@@ -5273,7 +5305,7 @@ export interface DashboardtypesAIBuilderQuerySpecDTO {
|
||||
bucketOptions?: Querybuildertypesv5BucketOptionsDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @description The opaque nextCursor value from a previous raw result, used to fetch the next page.
|
||||
* @description The opaque nextCursor value from a previous raw result, used to fetch the next page. Honored only by raw requests whose order starts with timestamp, for logs followed by id in the same direction; every other request ignores it.
|
||||
*/
|
||||
cursor?: string;
|
||||
/**
|
||||
@@ -5306,7 +5338,7 @@ export interface DashboardtypesAIBuilderQuerySpecDTO {
|
||||
legend?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @description The maximum number of rows for raw requests, or of series or rows for grouped aggregations, keeping the top entries by order. When 0, aggregations are unlimited while raw requests ordered by timestamp return a page of 10000 rows; follow nextCursor for the rest.
|
||||
* @description The maximum number of rows for raw and trace requests, or of series or rows for grouped scalar and time_series requests, keeping the top entries by order. When 0, scalar and time_series requests are unlimited, raw requests ordered by timestamp return a page of 10000 rows to continue with nextCursor, and other raw and trace requests return 100 rows.
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
@@ -5320,7 +5352,7 @@ export interface DashboardtypesAIBuilderQuerySpecDTO {
|
||||
name?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @description The number of rows to skip before returning results. Prefer cursor for raw requests.
|
||||
* @description The number of rows for raw and trace requests, or of ordered series or rows for scalar and time_series requests, to skip before the page starts. Requires limit. On raw requests the skip counts from where a cursor resumes, and the skipped rows are still read, so prefer cursor for paging.
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
@@ -5665,7 +5697,7 @@ export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTyp
|
||||
bucketOptions?: Querybuildertypesv5BucketOptionsDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @description The opaque nextCursor value from a previous raw result, used to fetch the next page.
|
||||
* @description The opaque nextCursor value from a previous raw result, used to fetch the next page. Honored only by raw requests whose order starts with timestamp, for logs followed by id in the same direction; every other request ignores it.
|
||||
*/
|
||||
cursor?: string;
|
||||
/**
|
||||
@@ -5698,7 +5730,7 @@ export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTyp
|
||||
legend?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @description The maximum number of rows for raw requests, or of series or rows for grouped aggregations, keeping the top entries by order. When 0, aggregations are unlimited while raw requests ordered by timestamp return a page of 10000 rows; follow nextCursor for the rest.
|
||||
* @description The maximum number of rows for raw and trace requests, or of series or rows for grouped scalar and time_series requests, keeping the top entries by order. When 0, scalar and time_series requests are unlimited, raw requests ordered by timestamp return a page of 10000 rows to continue with nextCursor, and other raw and trace requests return 100 rows.
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
@@ -5712,7 +5744,7 @@ export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTyp
|
||||
name?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @description The number of rows to skip before returning results. Prefer cursor for raw requests.
|
||||
* @description The number of rows for raw and trace requests, or of ordered series or rows for scalar and time_series requests, to skip before the page starts. Requires limit. On raw requests the skip counts from where a cursor resumes, and the skipped rows are still read, so prefer cursor for paging.
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
@@ -5845,7 +5877,7 @@ export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTyp
|
||||
bucketOptions?: Querybuildertypesv5BucketOptionsDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @description The opaque nextCursor value from a previous raw result, used to fetch the next page.
|
||||
* @description The opaque nextCursor value from a previous raw result, used to fetch the next page. Honored only by raw requests whose order starts with timestamp, for logs followed by id in the same direction; every other request ignores it.
|
||||
*/
|
||||
cursor?: string;
|
||||
/**
|
||||
@@ -5878,7 +5910,7 @@ export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTyp
|
||||
legend?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @description The maximum number of rows for raw requests, or of series or rows for grouped aggregations, keeping the top entries by order. When 0, aggregations are unlimited while raw requests ordered by timestamp return a page of 10000 rows; follow nextCursor for the rest.
|
||||
* @description The maximum number of rows for raw and trace requests, or of series or rows for grouped scalar and time_series requests, keeping the top entries by order. When 0, scalar and time_series requests are unlimited, raw requests ordered by timestamp return a page of 10000 rows to continue with nextCursor, and other raw and trace requests return 100 rows.
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
@@ -5892,7 +5924,7 @@ export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTyp
|
||||
name?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @description The number of rows to skip before returning results. Prefer cursor for raw requests.
|
||||
* @description The number of rows for raw and trace requests, or of ordered series or rows for scalar and time_series requests, to skip before the page starts. Requires limit. On raw requests the skip counts from where a cursor resumes, and the skipped rows are still read, so prefer cursor for paging.
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
@@ -5944,7 +5976,7 @@ export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTyp
|
||||
bucketOptions?: Querybuildertypesv5BucketOptionsDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @description The opaque nextCursor value from a previous raw result, used to fetch the next page.
|
||||
* @description The opaque nextCursor value from a previous raw result, used to fetch the next page. Honored only by raw requests whose order starts with timestamp, for logs followed by id in the same direction; every other request ignores it.
|
||||
*/
|
||||
cursor?: string;
|
||||
/**
|
||||
@@ -5977,7 +6009,7 @@ export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTyp
|
||||
legend?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @description The maximum number of rows for raw requests, or of series or rows for grouped aggregations, keeping the top entries by order. When 0, aggregations are unlimited while raw requests ordered by timestamp return a page of 10000 rows; follow nextCursor for the rest.
|
||||
* @description The maximum number of rows for raw and trace requests, or of series or rows for grouped scalar and time_series requests, keeping the top entries by order. When 0, scalar and time_series requests are unlimited, raw requests ordered by timestamp return a page of 10000 rows to continue with nextCursor, and other raw and trace requests return 100 rows.
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
@@ -5991,7 +6023,7 @@ export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTyp
|
||||
name?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @description The number of rows to skip before returning results. Prefer cursor for raw requests.
|
||||
* @description The number of rows for raw and trace requests, or of ordered series or rows for scalar and time_series requests, to skip before the page starts. Requires limit. On raw requests the skip counts from where a cursor resumes, and the skipped rows are still read, so prefer cursor for paging.
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
@@ -6937,6 +6969,7 @@ export enum Querybuildertypesv5RequestTypeDTO {
|
||||
raw_stream = 'raw_stream',
|
||||
trace = 'trace',
|
||||
heatmap = 'heatmap',
|
||||
distribution = 'distribution',
|
||||
}
|
||||
/**
|
||||
* The query kind.
|
||||
@@ -7064,6 +7097,11 @@ export interface Querybuildertypesv5QueryBuilderFormulaDTO {
|
||||
* @description The unique name of the formula, such as F1, used by selectedQueryName and by panels. Referencing a formula from another formula is not reliable.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @description The number of ordered series or rows to skip before the page starts. Requires limit.
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
* @type array,null
|
||||
* @description The sort order of the formula result.
|
||||
@@ -7146,7 +7184,7 @@ export interface Querybuildertypesv5QueryBuilderTraceOperatorDTO {
|
||||
name?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @description The number of raw results to skip before returning. Aggregation requests ignore it.
|
||||
* @description The number of raw or trace rows, or of ordered series or rows for time_series and scalar requests, to skip before the page starts. Requires limit.
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
@@ -10973,7 +11011,7 @@ export interface Querybuildertypesv5TimeSeriesValueDTO {
|
||||
value?: number;
|
||||
/**
|
||||
* @type array
|
||||
* @description The per-band counts for heatmap results, in the order of meta.buckets plus a trailing overflow count.
|
||||
* @description The per-bucket counts for heatmap results, in the order of meta.buckets plus a trailing overflow count.
|
||||
*/
|
||||
values?: number[];
|
||||
}
|
||||
@@ -11594,7 +11632,7 @@ export interface PromotetypesPostableIndexMaterializedPathsDTO {
|
||||
export interface Querybuildertypesv5AggregationMetaDTO {
|
||||
/**
|
||||
* @type array
|
||||
* @description The ascending upper bounds of the value bands, set only for heatmap results; each point then carries one count per bound plus a trailing overflow count in values.
|
||||
* @description The ascending upper bounds of the value buckets, set for heatmap and distribution results; each point then carries one count per bound plus a trailing overflow count in values. The first bucket starts at -Inf when its bound is at or under zero, else at zero.
|
||||
*/
|
||||
buckets?: number[];
|
||||
/**
|
||||
@@ -11715,6 +11753,91 @@ export interface Querybuildertypesv5ColumnDescriptorDTO {
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5DistributionDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
* @description One count per bound in meta.buckets plus a trailing overflow count, summed over every step of the requested time range.
|
||||
*/
|
||||
counts: number[] | null;
|
||||
/**
|
||||
* @type array
|
||||
* @description The group-by field values that identify this distribution.
|
||||
*/
|
||||
labels?: Querybuildertypesv5LabelDTO[];
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5DistributionPercentilesDTO {
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
* @description The 50th percentile.
|
||||
*/
|
||||
p50: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
* @description The 75th percentile.
|
||||
*/
|
||||
p75: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
* @description The 90th percentile.
|
||||
*/
|
||||
p90: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
* @description The 95th percentile.
|
||||
*/
|
||||
p95: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
* @description The 99th percentile.
|
||||
*/
|
||||
p99: number;
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5DistributionAggregationDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @description The alias of the aggregation, or the internal name __result_N, with N its index, when no alias was given.
|
||||
*/
|
||||
alias: string;
|
||||
/**
|
||||
* @type array,null
|
||||
* @description One distribution per group-by combination.
|
||||
*/
|
||||
distributions: Querybuildertypesv5DistributionDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @description The zero-based position of the aggregation in the query.
|
||||
*/
|
||||
index: number;
|
||||
/**
|
||||
* @description Display metadata for the aggregation, including the bucket upper bounds the counts are positional against.
|
||||
*/
|
||||
meta?: Querybuildertypesv5AggregationMetaDTO;
|
||||
/**
|
||||
* @description Percentiles over the counts of every distribution added together, estimated the way histogram_quantile does by interpolating linearly within the bucket each rank falls in. Omitted when nothing was counted or meta.buckets is empty.
|
||||
*/
|
||||
percentiles?: Querybuildertypesv5DistributionPercentilesDTO;
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5DistributionDataDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
* @description One entry per aggregation of the query. The order is not guaranteed; match entries to aggregations by index.
|
||||
*/
|
||||
aggregations: Querybuildertypesv5DistributionAggregationDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
* @description The name of the query that produced this result.
|
||||
*/
|
||||
queryName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The step interval in seconds that was used for each query, keyed by query name.
|
||||
*/
|
||||
@@ -11970,21 +12093,28 @@ export type Querybuildertypesv5QueryDataDTO =
|
||||
| (Querybuildertypesv5TimeSeriesDataDTO & {
|
||||
/**
|
||||
* @type array,null
|
||||
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series, ScalarData for scalar and RawData for raw requests.
|
||||
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw and DistributionData for distribution requests.
|
||||
*/
|
||||
results?: unknown[] | null;
|
||||
})
|
||||
| (Querybuildertypesv5ScalarDataDTO & {
|
||||
/**
|
||||
* @type array,null
|
||||
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series, ScalarData for scalar and RawData for raw requests.
|
||||
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw and DistributionData for distribution requests.
|
||||
*/
|
||||
results?: unknown[] | null;
|
||||
})
|
||||
| (Querybuildertypesv5RawDataDTO & {
|
||||
/**
|
||||
* @type array,null
|
||||
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series, ScalarData for scalar and RawData for raw requests.
|
||||
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw and DistributionData for distribution requests.
|
||||
*/
|
||||
results?: unknown[] | null;
|
||||
})
|
||||
| (Querybuildertypesv5DistributionDataDTO & {
|
||||
/**
|
||||
* @type array,null
|
||||
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw and DistributionData for distribution requests.
|
||||
*/
|
||||
results?: unknown[] | null;
|
||||
});
|
||||
@@ -12118,7 +12248,7 @@ export interface Querybuildertypesv5QueryRangeRequestDTO {
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from the v5 query range endpoint. The data.results array contains typed results depending on the requestType: TimeSeriesData for time_series, ScalarData for scalar, or RawData for raw requests.
|
||||
* Response from the v5 query range endpoint. The data.results array contains typed results depending on the requestType: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw, or DistributionData for distribution requests.
|
||||
*/
|
||||
export interface Querybuildertypesv5QueryRangeResponseDTO {
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,8 @@ import axios, { AxiosError, AxiosRequestConfig } from 'axios';
|
||||
// generated API Instance
|
||||
const generatedAPIAxiosInstance = axios.create({
|
||||
baseURL: ENVIRONMENT.baseURL,
|
||||
// The backend binds repeated keys (`states=a&states=b`); axios' default sends `states[]=`.
|
||||
paramsSerializer: { indexes: null },
|
||||
});
|
||||
|
||||
let generatedAPIQueryKeyHeaderContext: Record<string, unknown> | undefined;
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
.breadcrumb {
|
||||
padding: var(--spacing-4) 16px var(--spacing-2);
|
||||
|
||||
// The antd breadcrumb provides the item spacing.
|
||||
--button-padding: 0;
|
||||
|
||||
ol {
|
||||
align-items: center;
|
||||
}
|
||||
@@ -16,3 +13,8 @@
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
--divider-color: var(--l1-border);
|
||||
--divider-margin: 10px 0 16px 0;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ function AlertBreadcrumb({
|
||||
items={breadcrumbItems}
|
||||
data-testid={testId}
|
||||
/>
|
||||
{showDivider && <Divider spacing="10px 16px" />}
|
||||
{showDivider && <Divider className={styles.divider} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Copy } from '@signozhq/icons';
|
||||
import { Badge, type BadgeColorType } from '@signozhq/ui/badge';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import cx from 'classnames';
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Copy } from '@signozhq/icons';
|
||||
import { Badge, type BadgeColorType } from '@signozhq/ui/badge';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
|
||||
@@ -17,3 +17,8 @@
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.divider {
|
||||
--divider-color: var(--l1-border);
|
||||
--divider-margin: 10px 0 16px 0;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ function BackHeader({
|
||||
</Button>
|
||||
<div className={styles.title}>{title}</div>
|
||||
</div>
|
||||
<Divider spacing="10px 16px" />
|
||||
<Divider className={styles.divider} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,15 +56,22 @@ function ProgressRender(item: string | number): JSX.Element {
|
||||
const percent = Number(Number(item).toFixed(1));
|
||||
return (
|
||||
<div className="progress-container">
|
||||
<div className="progress-bar">
|
||||
<Progress
|
||||
percent={percent}
|
||||
showInfo
|
||||
color={
|
||||
percent >= 90 ? 'highlight-danger' : percent >= 60 ? 'warning' : 'success'
|
||||
<Progress
|
||||
percent={percent}
|
||||
strokeLinecap="butt"
|
||||
showInfo
|
||||
strokeColor={((): string => {
|
||||
const cpuPercent = percent;
|
||||
if (cpuPercent >= 90) {
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
if (cpuPercent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
})()}
|
||||
className="progress-bar"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function CeleryTaskDetail({
|
||||
endTime ? `- ${formatTimestamp(endTime)}` : ''
|
||||
}`}
|
||||
</Typography.Text>
|
||||
<Divider orientation="vertical" />
|
||||
<Divider type="vertical" />
|
||||
<Typography.Text className="subtitle">{taskData.value}</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,36 @@
|
||||
.create-sa-modal {
|
||||
max-width: 530px;
|
||||
background: var(--popover);
|
||||
border: 1px solid var(--secondary);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 9px 0 rgba(0, 0, 0, 0.04);
|
||||
|
||||
[data-slot='dialog-header'] {
|
||||
padding: var(--padding-4);
|
||||
border-bottom: 1px solid var(--secondary);
|
||||
flex-shrink: 0;
|
||||
background: transparent;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-slot='dialog-title'] {
|
||||
font-size: var(--label-base-400-font-size);
|
||||
font-weight: var(--label-base-400-font-weight);
|
||||
line-height: var(--label-base-400-line-height);
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--l1-foreground);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-slot='dialog-description'] {
|
||||
padding: 0;
|
||||
|
||||
.create-sa-modal__content {
|
||||
padding: var(--padding-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.create-sa-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -17,6 +50,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
height: 32px;
|
||||
color: var(--l1-foreground);
|
||||
background-color: var(--l2-background);
|
||||
border-color: var(--l1-border);
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
border-radius: 2px;
|
||||
width: 100%;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__error {
|
||||
font-size: var(--paragraph-small-400-font-size);
|
||||
color: var(--destructive);
|
||||
@@ -31,3 +83,16 @@
|
||||
line-height: var(--paragraph-small-400-line-height);
|
||||
}
|
||||
}
|
||||
|
||||
.create-sa-modal__footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 var(--padding-4);
|
||||
height: 56px;
|
||||
min-height: 56px;
|
||||
border-top: 1px solid var(--secondary);
|
||||
gap: var(--spacing-4);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AuthZGuardContent } from 'lib/authz/components/AuthZGuard/AuthZGuardCon
|
||||
import { SACreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { DialogFooter, DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
invalidateListServiceAccounts,
|
||||
@@ -116,10 +116,11 @@ function CreateServiceAccountModal(): JSX.Element {
|
||||
<Input
|
||||
id="sa-name"
|
||||
placeholder="Enter a name"
|
||||
className="create-sa-form__input"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
testId="create-sa-name-input"
|
||||
data-testid="create-sa-name-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import {
|
||||
setupAuthzAdmin,
|
||||
setupAuthzDenyAll,
|
||||
@@ -10,9 +10,9 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import CreateServiceAccountModal from '../CreateServiceAccountModal';
|
||||
|
||||
jest.mock('@signozhq/ui/toast', () => ({
|
||||
...jest.requireActual('@signozhq/ui/toast'),
|
||||
toast: { success: jest.fn(), danger: jest.fn() },
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
const mockToast = jest.mocked(toast);
|
||||
@@ -58,7 +58,7 @@ describe('CreateServiceAccountModal', () => {
|
||||
// The form only renders once the create check resolves, and the name field
|
||||
// registers its `required` rule on mount, so the empty-form invalid state
|
||||
// settles a tick later.
|
||||
await screen.findByTestId('create-sa-name-input-field');
|
||||
await screen.findByTestId('create-sa-name-input');
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('create-sa-submit-btn')).toHaveAttribute(
|
||||
@@ -72,7 +72,7 @@ describe('CreateServiceAccountModal', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderModal();
|
||||
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input-field');
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input');
|
||||
const submitBtn = await screen.findByTestId('create-sa-submit-btn');
|
||||
|
||||
await user.type(nameInput, 'test');
|
||||
@@ -90,7 +90,7 @@ describe('CreateServiceAccountModal', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderModal();
|
||||
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input-field');
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input');
|
||||
await user.type(nameInput, 'Deploy Bot');
|
||||
|
||||
const submitBtn = screen.getByTestId('create-sa-submit-btn');
|
||||
@@ -128,7 +128,7 @@ describe('CreateServiceAccountModal', () => {
|
||||
renderModal();
|
||||
|
||||
const nameInput = await screen.findByTestId<HTMLInputElement>(
|
||||
'create-sa-name-input-field',
|
||||
'create-sa-name-input',
|
||||
);
|
||||
await user.type(nameInput, 'Deploy Bot');
|
||||
const submitBtn = screen.getByTestId('create-sa-submit-btn');
|
||||
@@ -160,7 +160,7 @@ describe('CreateServiceAccountModal', () => {
|
||||
|
||||
renderModal();
|
||||
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input-field');
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input');
|
||||
await user.type(nameInput, 'Dupe Bot');
|
||||
|
||||
const submitBtn = screen.getByTestId('create-sa-submit-btn');
|
||||
@@ -209,9 +209,7 @@ describe('CreateServiceAccountModal', () => {
|
||||
screen.findByText(/is not authorized to perform/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('create-sa-name-input-field'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('create-sa-name-input')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('create-service-account-modal'),
|
||||
).toBeInTheDocument();
|
||||
@@ -246,7 +244,7 @@ describe('CreateServiceAccountModal', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderModal();
|
||||
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input-field');
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input');
|
||||
await user.type(nameInput, 'Bot');
|
||||
await user.clear(nameInput);
|
||||
|
||||
|
||||
@@ -34,10 +34,34 @@ $item-spacing: 8px;
|
||||
@include flex-center;
|
||||
gap: 6px;
|
||||
width: -webkit-fill-available;
|
||||
}
|
||||
|
||||
// The search row draws its own padding; the inline Input hugs the text.
|
||||
--input-height: auto;
|
||||
--input-padding: 0;
|
||||
&__input {
|
||||
@include text-style-base;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
height: auto;
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
padding: 0;
|
||||
|
||||
&:focus,
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__esc-key {
|
||||
|
||||
@@ -65,8 +65,7 @@ function SearchBar({
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
variant="unstyled"
|
||||
noFocusRing
|
||||
className="timezone-picker__input"
|
||||
placeholder="Search timezones..."
|
||||
value={value}
|
||||
onChange={(e): void => onChange(e.target.value)}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
.details-header {
|
||||
// ghost + secondary missing hover bg token in @signozhq/button
|
||||
--button-ghost-hover-background: var(--l3-background);
|
||||
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DrawerWrapper } from '@signozhq/ui/drawer';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Skeleton, Tooltip } from 'antd';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
@@ -409,8 +409,7 @@ function EditMemberDrawer({
|
||||
setShowResetLinkDialog(true);
|
||||
onClose();
|
||||
} else {
|
||||
toast.danger('Failed to generate password reset link', {
|
||||
action: { label: 'Dismiss' },
|
||||
toast.error('Failed to generate password reset link', {
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
@@ -439,7 +438,7 @@ function EditMemberDrawer({
|
||||
|
||||
useEffect(() => {
|
||||
if (copyState.error) {
|
||||
toast.danger('Failed to copy link', { action: { label: 'Dismiss' } });
|
||||
toast.error('Failed to copy link');
|
||||
}
|
||||
}, [copyState.error]);
|
||||
|
||||
@@ -472,19 +471,20 @@ function EditMemberDrawer({
|
||||
<label className="edit-member-drawer__label" htmlFor="member-name">
|
||||
Name
|
||||
</label>
|
||||
<Input
|
||||
id="member-name"
|
||||
value={localDisplayName}
|
||||
onChange={(e): void => {
|
||||
setLocalDisplayName(e.target.value);
|
||||
setSaveErrors((prev) =>
|
||||
prev.filter((err) => err.context !== 'Name update'),
|
||||
);
|
||||
}}
|
||||
placeholder="Enter name"
|
||||
disabled={isRootUser || isDeleted}
|
||||
disabledTooltip={isRootUser ? ROOT_USER_TOOLTIP : undefined}
|
||||
/>
|
||||
<Tooltip title={isRootUser ? ROOT_USER_TOOLTIP : undefined}>
|
||||
<Input
|
||||
id="member-name"
|
||||
value={localDisplayName}
|
||||
onChange={(e): void => {
|
||||
setLocalDisplayName(e.target.value);
|
||||
setSaveErrors((prev) =>
|
||||
prev.filter((err) => err.context !== 'Name update'),
|
||||
);
|
||||
}}
|
||||
placeholder="Enter name"
|
||||
disabled={isRootUser || isDeleted}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__field">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
useCreateResetPasswordToken,
|
||||
@@ -82,11 +82,11 @@ jest.mock('@signozhq/ui/dialog', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/ui/toast', () => ({
|
||||
...jest.requireActual('@signozhq/ui/toast'),
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: {
|
||||
success: jest.fn(),
|
||||
danger: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -3,7 +3,20 @@
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 20px;
|
||||
background: color-mix(in srgb, var(--danger-background) 20%, transparent);
|
||||
padding-left: 3px;
|
||||
padding-right: 8px;
|
||||
cursor: pointer;
|
||||
span {
|
||||
color: var(--danger-background);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 200% */
|
||||
letter-spacing: 0.4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
&__wrap {
|
||||
background: linear-gradient(
|
||||
|
||||
@@ -53,13 +53,8 @@ function ErrorModal({
|
||||
onClick={(): void => setVisible(true)}
|
||||
onKeyDown={undefined}
|
||||
>
|
||||
<Badge
|
||||
variant="solid"
|
||||
color="danger"
|
||||
textTransform="uppercase"
|
||||
prefix={<CircleAlert size={14} />}
|
||||
>
|
||||
error
|
||||
<Badge variant="solid" color="danger">
|
||||
<CircleAlert size={14} color={Color.BG_CHERRY_500} /> error
|
||||
</Badge>
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.export-menu-popover {
|
||||
// The popover shell carries no padding of its own, so the content owns it.
|
||||
padding: var(--spacing-8);
|
||||
width: 240px;
|
||||
padding: 0 12px 12px 12px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Download } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Popover, usePopoverHandle } from '@signozhq/ui/popover';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
|
||||
import { RadioGroup } from '@signozhq/ui/radio-group';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
@@ -36,10 +36,6 @@ export default function ExportMenu({
|
||||
}: ExportMenuProps): JSX.Element {
|
||||
const [exportFormat, setExportFormat] = useState<string>(ExportFormat.Csv);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
// Driven by a handle rather than `trigger`: the button carries a Tooltip, and a Tooltip
|
||||
// routes every prop it does not recognise to its own popup, so a popover's press handlers
|
||||
// would never reach the button underneath.
|
||||
const menu = usePopoverHandle(setIsPopoverOpen);
|
||||
|
||||
const { isExporting, handleExport: handleClientExport } = useClientExport({
|
||||
data,
|
||||
@@ -49,62 +45,53 @@ export default function ExportMenu({
|
||||
});
|
||||
|
||||
const handleExport = useCallback((): void => {
|
||||
menu.close();
|
||||
setIsPopoverOpen(false);
|
||||
handleClientExport({ format: exportFormat as ExportFormat });
|
||||
}, [menu, exportFormat, handleClientExport]);
|
||||
}, [exportFormat, handleClientExport]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||
<Tooltip title="Download">
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon
|
||||
aria-label="Download"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={isPopoverOpen}
|
||||
testId={`export-menu-${dataSource}`}
|
||||
loading={isExporting}
|
||||
onClick={(event): void => menu.showAt(event.currentTarget)}
|
||||
>
|
||||
<Download size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
side="bottom"
|
||||
align="end"
|
||||
width={240}
|
||||
handle={menu}
|
||||
aria-label="Export format"
|
||||
>
|
||||
<div className="export-menu-popover">
|
||||
<div className="export-format">
|
||||
<Typography.Text className="title">FORMAT</Typography.Text>
|
||||
<RadioGroup
|
||||
color="primary"
|
||||
value={exportFormat}
|
||||
onChange={setExportFormat}
|
||||
items={[
|
||||
{ value: ExportFormat.Csv, label: 'csv' },
|
||||
{ value: ExportFormat.Jsonl, label: 'jsonl' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
size="md"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
width="100%"
|
||||
onClick={handleExport}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon
|
||||
aria-label="Download"
|
||||
testId={`export-menu-${dataSource}`}
|
||||
loading={isExporting}
|
||||
prefix={<Download size={16} />}
|
||||
>
|
||||
Export
|
||||
<Download size={14} />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</Tooltip>
|
||||
<PopoverContent align="end" className="export-menu-popover">
|
||||
<div className="export-format">
|
||||
<Typography.Text className="title">FORMAT</Typography.Text>
|
||||
<RadioGroup
|
||||
color="primary"
|
||||
value={exportFormat}
|
||||
onChange={setExportFormat}
|
||||
items={[
|
||||
{ value: ExportFormat.Csv, label: 'csv' },
|
||||
{ value: ExportFormat.Jsonl, label: 'jsonl' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
</>
|
||||
|
||||
<Button
|
||||
size="md"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
width="100%"
|
||||
onClick={handleExport}
|
||||
loading={isExporting}
|
||||
prefix={<Download size={16} />}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,15 +32,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Full-bleed search bar: only top/bottom borders, square corners.
|
||||
// Per-side widths cannot be expressed via --input-border-width.
|
||||
.searchInput {
|
||||
--input-border-radius: 0;
|
||||
|
||||
[data-slot='input'] {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
background-color: var(--l1-background);
|
||||
height: 40px;
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.section {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
@@ -140,10 +140,10 @@ function FieldsSelectorContent({
|
||||
<X className={styles.closeIcon} size={16} onClick={onClose} />
|
||||
</div>
|
||||
|
||||
<section className={styles.searchInput}>
|
||||
<section>
|
||||
<Input
|
||||
className={styles.searchInput}
|
||||
type="text"
|
||||
size="large"
|
||||
value={inputValue}
|
||||
placeholder="Search for a field..."
|
||||
onChange={handleInputChange}
|
||||
|
||||
@@ -13,9 +13,9 @@ jest.mock('hooks/querySuggestions/useFieldKeysSuggestion', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/ui/toast', () => ({
|
||||
...jest.requireActual('@signozhq/ui/toast'),
|
||||
toast: { success: jest.fn(), danger: jest.fn() },
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
// FloatingPanel is a react-rnd/portal shell — presentation only. Render its
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Button, Input } from 'antd';
|
||||
import { ToggleGroup } from '@signozhq/ui/toggle-group';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
@@ -39,8 +39,7 @@ function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
|
||||
})
|
||||
.catch(() => {
|
||||
console.error(`Failed to submit ${entityName}`);
|
||||
toast.danger(`Failed to submit ${entityName}`, {
|
||||
action: { label: 'Dismiss' },
|
||||
toast.error(`Failed to submit ${entityName}`, {
|
||||
position: 'top-right',
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Mock dependencies before imports
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { logEventMock } from '__tests__/logEventMock';
|
||||
@@ -13,11 +13,11 @@ jest.mock('lib/router/useAppLocation', () => ({
|
||||
useAppLocation: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/ui/toast', () => ({
|
||||
...jest.requireActual('@signozhq/ui/toast'),
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: {
|
||||
success: jest.fn(),
|
||||
danger: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('FeedbackModal', () => {
|
||||
isCloudUser: false,
|
||||
});
|
||||
mockToast.success.mockClear();
|
||||
mockToast.danger.mockClear();
|
||||
mockToast.error.mockClear();
|
||||
});
|
||||
|
||||
it('should render feedback modal with all tabs', () => {
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
|
||||
// The label chip and the Input share the --input-with-label-* theming
|
||||
// contract (set by e.g. FormAlertRules, WidgetFullView) so the joined
|
||||
// group recolors as one.
|
||||
.label {
|
||||
// Typography.Text takes its display from this token; at its `inline` default
|
||||
// the label blockifies as a flex item and the text rides the top of the row.
|
||||
@@ -34,40 +31,66 @@
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
font-weight: var(--font-weight-light);
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
|
||||
--input-border-radius: 0px;
|
||||
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
|
||||
background: var(--input-with-label-background-color, var(--l2-background));
|
||||
color: var(--input-with-label-color, var(--l2-foreground));
|
||||
|
||||
font-size: 12px !important;
|
||||
line-height: 25px;
|
||||
|
||||
position: relative;
|
||||
|
||||
// Square corners: the input sits mid-group between label and close button.
|
||||
--input-border-radius: 0px;
|
||||
--input-border: var(--input-with-label-border-color, var(--l2-border));
|
||||
--input-background: var(
|
||||
--input-with-label-background-color,
|
||||
var(--l2-background)
|
||||
);
|
||||
--input-foreground: var(--input-with-label-color, var(--l2-foreground));
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
&:focus {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
position: relative;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.ant-select:hover .ant-select-selector,
|
||||
.ant-select-focused .ant-select-selector {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.input__has-label-after {
|
||||
margin-left: -1px;
|
||||
|
||||
.ant-select-selector {
|
||||
margin-left: -1px;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.input__has-close-button {
|
||||
.ant-select-selector {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
~ .close-btn {
|
||||
margin-left: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
// The Input has no token for the native number spinners.
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
&::placeholder {
|
||||
color: var(--input-with-label-color, var(--l3-foreground)) !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
&[type='number']::-webkit-inner-spin-button,
|
||||
&[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
@@ -91,7 +114,16 @@
|
||||
|
||||
&.labelAfter {
|
||||
.input {
|
||||
--input-border-radius: 2px 0 0 2px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
|
||||
background: var(--input-with-label-background-color, var(--l2-background));
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
|
||||
.ant-select-selector {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
|
||||
@@ -50,23 +50,19 @@ function InputWithLabel({
|
||||
})}
|
||||
>
|
||||
{!labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
|
||||
<div
|
||||
<Input
|
||||
className={cx('input', {
|
||||
'input__has-label-after': !labelAfter,
|
||||
'input__has-close-button': !!onClose,
|
||||
})}
|
||||
>
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
disabledTooltip={undefined}
|
||||
name={label.toLowerCase()}
|
||||
testId={`input-${label}`}
|
||||
/>
|
||||
</div>
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
name={label.toLowerCase()}
|
||||
data-testid={`input-${label}`}
|
||||
/>
|
||||
{labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
|
||||
{onClose && (
|
||||
<Button
|
||||
|
||||
@@ -55,12 +55,26 @@
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
|
||||
// Matches the antd RolesSelect in the same row, which is forced to the
|
||||
// same field background below.
|
||||
--input-background: var(
|
||||
--invite-members-field-background,
|
||||
var(--l2-background)
|
||||
);
|
||||
--input-hover-background: var(
|
||||
--invite-members-field-background,
|
||||
var(--l2-background)
|
||||
);
|
||||
--input-focus-background: var(
|
||||
--invite-members-field-background,
|
||||
var(--l2-background)
|
||||
);
|
||||
--input-disabled-background: var(
|
||||
--invite-members-field-background,
|
||||
var(--l2-background)
|
||||
);
|
||||
|
||||
input::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.cellRole {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CircleAlert, Plus, SolidCheckCircle2, Trash2 } from '@signozhq/icons';
|
||||
import { CircleAlert, Plus, Trash2 } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
@@ -97,7 +97,7 @@ function InviteMembers({
|
||||
onChange={(e): void => updateEmail(row.id, e.target.value)}
|
||||
name={`invite-email-${row.id}`}
|
||||
autoComplete="email"
|
||||
testId={`invite-email-${row.id}`}
|
||||
data-testid={`invite-email-${row.id}`}
|
||||
/>
|
||||
{emailValidity[row.id] === false && row.email.trim() !== '' && (
|
||||
<Typography.Text size="small" className={styles.errorText}>
|
||||
@@ -152,62 +152,61 @@ function InviteMembers({
|
||||
</div>
|
||||
|
||||
{hasValidationErrors && (
|
||||
<div className={styles.callout}>
|
||||
<Callout
|
||||
color="danger"
|
||||
size="sm"
|
||||
icon={<CircleAlert />}
|
||||
testId="invite-validation-error"
|
||||
>
|
||||
{getValidationErrorMessage()}
|
||||
</Callout>
|
||||
</div>
|
||||
<Callout
|
||||
type="error"
|
||||
size="small"
|
||||
showIcon
|
||||
icon={<CircleAlert size={12} />}
|
||||
className={styles.callout}
|
||||
data-testid="invite-validation-error"
|
||||
>
|
||||
{getValidationErrorMessage()}
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
{hasResults && hasFailures && (
|
||||
<div className={styles.callout}>
|
||||
<Callout
|
||||
color="danger"
|
||||
size="sm"
|
||||
icon={<CircleAlert />}
|
||||
testId="invite-api-error"
|
||||
>
|
||||
<div className={styles.results}>
|
||||
{hasSuccesses && (
|
||||
<Typography.Text size="small">
|
||||
{successResults.length} invite(s) sent successfully.
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Callout
|
||||
type="error"
|
||||
size="small"
|
||||
showIcon
|
||||
icon={<CircleAlert size={12} />}
|
||||
className={styles.callout}
|
||||
data-testid="invite-api-error"
|
||||
>
|
||||
<div className={styles.results}>
|
||||
{hasSuccesses && (
|
||||
<Typography.Text size="small">
|
||||
{failedResults.length} invite(s) failed:
|
||||
{successResults.length} invite(s) sent successfully.
|
||||
</Typography.Text>
|
||||
<ul className={styles.resultsList}>
|
||||
{failedResults.map((result) => (
|
||||
<li key={result.email}>
|
||||
<Typography.Text size="small">
|
||||
{result.email}: {result.error}
|
||||
</Typography.Text>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Callout>
|
||||
</div>
|
||||
)}
|
||||
<Typography.Text size="small">
|
||||
{failedResults.length} invite(s) failed:
|
||||
</Typography.Text>
|
||||
<ul className={styles.resultsList}>
|
||||
{failedResults.map((result) => (
|
||||
<li key={result.email}>
|
||||
<Typography.Text size="small">
|
||||
{result.email}: {result.error}
|
||||
</Typography.Text>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
{hasResults && !hasFailures && hasSuccesses && (
|
||||
<div className={styles.callout}>
|
||||
<Callout
|
||||
color="success"
|
||||
size="sm"
|
||||
icon={<SolidCheckCircle2 />}
|
||||
testId="invite-success"
|
||||
>
|
||||
<Typography.Text size="small">
|
||||
{successResults.length} invite(s) sent successfully!
|
||||
</Typography.Text>
|
||||
</Callout>
|
||||
</div>
|
||||
<Callout
|
||||
type="success"
|
||||
size="small"
|
||||
showIcon
|
||||
className={styles.callout}
|
||||
data-testid="invite-success"
|
||||
>
|
||||
<Typography.Text size="small">
|
||||
{successResults.length} invite(s) sent successfully!
|
||||
</Typography.Text>
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
{renderFooter?.({
|
||||
|
||||
100
frontend/src/components/ListFilter/FilterZone/FilterChips.tsx
Normal file
100
frontend/src/components/ListFilter/FilterZone/FilterChips.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { ConfigProvider, Select } from 'antd';
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
import cx from 'classnames';
|
||||
|
||||
import type { UpdatedWindow } from '../dsl/types';
|
||||
import type { CreatorOption } from '../types';
|
||||
import { UPDATED_OPTIONS } from './constants';
|
||||
|
||||
import styles from './FilterZone.module.scss';
|
||||
|
||||
// The option checkbox already marks selection, so selected rows stay regular weight.
|
||||
const CREATOR_SELECT_THEME = {
|
||||
components: { Select: { optionSelectedFontWeight: 400 } },
|
||||
};
|
||||
|
||||
interface Props {
|
||||
createdBy: string[];
|
||||
updated: UpdatedWindow;
|
||||
creatorOptions: CreatorOption[];
|
||||
onCreatedByChange: (emails: string[]) => void;
|
||||
onUpdatedChange: (window: UpdatedWindow) => void;
|
||||
// Run the staged draft — fired when a dropdown closes.
|
||||
onApply: () => void;
|
||||
// Clear all Created-by selections and run immediately.
|
||||
onClearCreatedBy: () => void;
|
||||
// Ids: `${testIdPrefix}-filter-created-by`, `-filter-updated`.
|
||||
testIdPrefix: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function FilterChips({
|
||||
createdBy,
|
||||
updated,
|
||||
creatorOptions,
|
||||
onCreatedByChange,
|
||||
onUpdatedChange,
|
||||
onApply,
|
||||
onClearCreatedBy,
|
||||
testIdPrefix,
|
||||
disabled = false,
|
||||
}: Props): JSX.Element {
|
||||
const creatorOptionsData = creatorOptions.map((o) => ({
|
||||
value: o.email,
|
||||
label: o.label,
|
||||
}));
|
||||
|
||||
const runOnClose = (open: boolean): void => {
|
||||
if (!open) {
|
||||
onApply();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.chips}>
|
||||
<ConfigProvider theme={CREATOR_SELECT_THEME}>
|
||||
<Select
|
||||
mode="multiple"
|
||||
showSearch
|
||||
allowClear
|
||||
className={cx(styles.select, styles.selectWide)}
|
||||
placeholder="Created by"
|
||||
value={createdBy}
|
||||
options={creatorOptionsData}
|
||||
optionFilterProp="label"
|
||||
maxTagCount={1}
|
||||
menuItemSelectedIcon={null}
|
||||
data-testid={`${testIdPrefix}-filter-created-by`}
|
||||
disabled={disabled}
|
||||
onClear={onClearCreatedBy}
|
||||
optionRender={(option): JSX.Element => (
|
||||
<div className={styles.creatorOption}>
|
||||
<div className={styles.creatorCheck}>
|
||||
<Checkbox
|
||||
color="primary"
|
||||
value={createdBy.includes(option.value as string)}
|
||||
/>
|
||||
</div>
|
||||
<span className={styles.creatorLabel}>{option.label}</span>
|
||||
</div>
|
||||
)}
|
||||
onChange={(value): void => onCreatedByChange(value as string[])}
|
||||
onDropdownVisibleChange={runOnClose}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
<Select
|
||||
showSearch
|
||||
className={cx(styles.select, styles.selectNarrow)}
|
||||
placeholder="Updated"
|
||||
value={updated}
|
||||
options={UPDATED_OPTIONS}
|
||||
optionFilterProp="label"
|
||||
data-testid={`${testIdPrefix}-filter-updated`}
|
||||
disabled={disabled}
|
||||
onChange={(value): void => onUpdatedChange(value as UpdatedWindow)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilterChips;
|
||||
@@ -94,6 +94,8 @@
|
||||
}
|
||||
|
||||
.creatorCheck {
|
||||
// Flex, not block: a line box would shift with the check mark's baseline.
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -103,3 +105,8 @@
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--danger-foreground);
|
||||
}
|
||||
166
frontend/src/components/ListFilter/FilterZone/FilterZone.tsx
Normal file
166
frontend/src/components/ListFilter/FilterZone/FilterZone.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { X } from '@signozhq/icons';
|
||||
|
||||
import type { DslGrammar, SuggestionSource } from '../dsl/types';
|
||||
import SearchBar from '../SearchBar/SearchBar';
|
||||
import type {
|
||||
CreatorOption,
|
||||
FilterAppliedPayload,
|
||||
FilterAppliedType,
|
||||
} from '../types';
|
||||
import FilterChips from './FilterChips';
|
||||
import { useFilterZoneDraft } from './useFilterZoneDraft';
|
||||
|
||||
import styles from './FilterZone.module.scss';
|
||||
|
||||
interface Props {
|
||||
// The last-run query (source of truth for fetching + the dirty baseline).
|
||||
query: string;
|
||||
grammar: DslGrammar;
|
||||
// Ids: `${testIdPrefix}-search-zone`, `-list-search*`, `-filter-zone`,
|
||||
// `-filter-clear`, `-filter-created-by`, `-filter-updated`, `-query-error`.
|
||||
testIdPrefix: string;
|
||||
creatorOptions: CreatorOption[];
|
||||
source: SuggestionSource;
|
||||
// Commit (run) the draft — the only path that triggers a fetch.
|
||||
onQueryChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
// Rendered at the end of the search row (e.g. the New Dashboard action).
|
||||
rightSlot?: ReactNode;
|
||||
// Rendered in the filters row after the chips.
|
||||
extraFilters?: ReactNode;
|
||||
// Shows Clear even when the query is empty.
|
||||
hasExtraFilters?: boolean;
|
||||
// Called by Clear after it empties the query.
|
||||
onClearAll?: () => void;
|
||||
// Shown under the search row.
|
||||
error?: string;
|
||||
// Wraps the search box and the filter controls (e.g. in an authz tooltip).
|
||||
wrapControl?: (node: ReactElement) => ReactElement;
|
||||
onSubmitted?: (hasQuery: boolean) => void;
|
||||
onFilterApplied?: (
|
||||
filterType: FilterAppliedType,
|
||||
payload: FilterAppliedPayload,
|
||||
) => void;
|
||||
onCleared?: () => void;
|
||||
/** Filtering needs `list`: the box and chips edit a query that cannot be run. */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const identity = (node: ReactElement): ReactElement => node;
|
||||
|
||||
// The filter command zone: a draft DSL query box (run with Cmd/Ctrl+Enter or the
|
||||
// Run button; a dirty dot signals unrun changes) plus Created-by / Updated chips.
|
||||
function FilterZone({
|
||||
query,
|
||||
grammar,
|
||||
testIdPrefix,
|
||||
creatorOptions,
|
||||
source,
|
||||
onQueryChange,
|
||||
placeholder,
|
||||
rightSlot,
|
||||
extraFilters,
|
||||
hasExtraFilters = false,
|
||||
onClearAll,
|
||||
error,
|
||||
wrapControl = identity,
|
||||
onSubmitted,
|
||||
onFilterApplied,
|
||||
onCleared,
|
||||
disabled = false,
|
||||
}: Props): JSX.Element {
|
||||
const {
|
||||
draft,
|
||||
setDraft,
|
||||
reflected,
|
||||
dirty,
|
||||
isEmpty,
|
||||
run,
|
||||
handleCreatedByChange,
|
||||
handleUpdatedChange,
|
||||
handleClearCreatedBy,
|
||||
handleClear,
|
||||
} = useFilterZoneDraft({
|
||||
query,
|
||||
onQueryChange,
|
||||
onFilterApplied,
|
||||
onCleared,
|
||||
onClearAll,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.filterZone}>
|
||||
<div className={styles.searchRow}>
|
||||
{wrapControl(
|
||||
<div
|
||||
className={styles.searchInput}
|
||||
data-testid={`${testIdPrefix}-search-zone`}
|
||||
>
|
||||
<SearchBar
|
||||
value={draft}
|
||||
grammar={grammar}
|
||||
testIdPrefix={`${testIdPrefix}-list-search`}
|
||||
placeholder={placeholder}
|
||||
source={source}
|
||||
dirty={dirty}
|
||||
disabled={disabled}
|
||||
onChange={setDraft}
|
||||
onSubmit={run}
|
||||
onSubmitted={onSubmitted}
|
||||
/>
|
||||
</div>,
|
||||
)}
|
||||
{rightSlot}
|
||||
</div>
|
||||
{error && (
|
||||
<Typography.Text
|
||||
className={styles.error}
|
||||
testId={`${testIdPrefix}-query-error`}
|
||||
>
|
||||
{error}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<div className={styles.filtersRow}>
|
||||
<Typography.Text className={styles.filtersLabel}>Filters</Typography.Text>
|
||||
{wrapControl(
|
||||
<div
|
||||
className={styles.filterControls}
|
||||
data-testid={`${testIdPrefix}-filter-zone`}
|
||||
>
|
||||
<FilterChips
|
||||
createdBy={reflected.createdBy}
|
||||
updated={reflected.updated}
|
||||
creatorOptions={creatorOptions}
|
||||
testIdPrefix={testIdPrefix}
|
||||
onCreatedByChange={handleCreatedByChange}
|
||||
onUpdatedChange={handleUpdatedChange}
|
||||
onApply={run}
|
||||
onClearCreatedBy={handleClearCreatedBy}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{extraFilters}
|
||||
{(!isEmpty || hasExtraFilters) && (
|
||||
<Button
|
||||
disabledTooltip={undefined}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="md"
|
||||
prefix={<X size={12} />}
|
||||
disabled={disabled}
|
||||
onClick={handleClear}
|
||||
testId={`${testIdPrefix}-filter-clear`}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
</div>,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilterZone;
|
||||
@@ -0,0 +1,135 @@
|
||||
import { act, renderHook } from 'tests/test-utils';
|
||||
|
||||
import { useFilterZoneDraft } from '../useFilterZoneDraft';
|
||||
|
||||
const BASE_QUERY = "name = 'x'";
|
||||
|
||||
type Args = Required<Parameters<typeof useFilterZoneDraft>[0]>;
|
||||
|
||||
function setup(query = BASE_QUERY): {
|
||||
result: { current: ReturnType<typeof useFilterZoneDraft> };
|
||||
rerender: (props: { query: string }) => void;
|
||||
onQueryChange: jest.MockedFunction<Args['onQueryChange']>;
|
||||
onFilterApplied: jest.MockedFunction<Args['onFilterApplied']>;
|
||||
onCleared: jest.MockedFunction<Args['onCleared']>;
|
||||
onClearAll: jest.MockedFunction<Args['onClearAll']>;
|
||||
} {
|
||||
const onQueryChange = jest.fn() as jest.MockedFunction<Args['onQueryChange']>;
|
||||
const onFilterApplied = jest.fn() as jest.MockedFunction<
|
||||
Args['onFilterApplied']
|
||||
>;
|
||||
const onCleared = jest.fn() as jest.MockedFunction<Args['onCleared']>;
|
||||
const onClearAll = jest.fn() as jest.MockedFunction<Args['onClearAll']>;
|
||||
const { result, rerender } = renderHook(
|
||||
({ query: q }: { query: string }) =>
|
||||
useFilterZoneDraft({
|
||||
query: q,
|
||||
onQueryChange,
|
||||
onFilterApplied,
|
||||
onCleared,
|
||||
onClearAll,
|
||||
}),
|
||||
{ initialProps: { query } },
|
||||
);
|
||||
return {
|
||||
result,
|
||||
rerender,
|
||||
onQueryChange,
|
||||
onFilterApplied,
|
||||
onCleared,
|
||||
onClearAll,
|
||||
};
|
||||
}
|
||||
|
||||
describe('useFilterZoneDraft', () => {
|
||||
it('edits a draft without running it until run is called', () => {
|
||||
const { result, onQueryChange } = setup();
|
||||
|
||||
act(() => result.current.setDraft("name = 'y' "));
|
||||
|
||||
expect(result.current.dirty).toBe(true);
|
||||
expect(onQueryChange).not.toHaveBeenCalled();
|
||||
|
||||
act(() => result.current.run());
|
||||
|
||||
expect(onQueryChange).toHaveBeenCalledWith("name = 'y'");
|
||||
});
|
||||
|
||||
it('does not run a draft that matches the last-run query', () => {
|
||||
const { result, onQueryChange } = setup();
|
||||
|
||||
act(() => result.current.run());
|
||||
|
||||
expect(result.current.dirty).toBe(false);
|
||||
expect(onQueryChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resyncs the draft when the query changes from outside', () => {
|
||||
const { result, rerender } = setup();
|
||||
|
||||
act(() => result.current.setDraft("name = 'typing'"));
|
||||
rerender({ query: "name = 'from-view'" });
|
||||
|
||||
expect(result.current.draft).toBe("name = 'from-view'");
|
||||
expect(result.current.dirty).toBe(false);
|
||||
});
|
||||
|
||||
it('stages a created-by pick into the draft without running it', () => {
|
||||
const { result, onQueryChange, onFilterApplied } = setup();
|
||||
|
||||
act(() => result.current.handleCreatedByChange(['a@x.io', 'b@x.io']));
|
||||
|
||||
expect(result.current.draft).toBe(
|
||||
"name = 'x' AND created_by IN ['a@x.io', 'b@x.io']",
|
||||
);
|
||||
expect(result.current.reflected.createdBy).toStrictEqual([
|
||||
'a@x.io',
|
||||
'b@x.io',
|
||||
]);
|
||||
expect(onFilterApplied).toHaveBeenCalledWith('createdBy', { valueCount: 2 });
|
||||
expect(onQueryChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('runs an updated-window pick immediately', () => {
|
||||
const { result, onQueryChange, onFilterApplied } = setup();
|
||||
|
||||
act(() => result.current.handleUpdatedChange('7d'));
|
||||
|
||||
expect(onFilterApplied).toHaveBeenCalledWith('updatedWindow', {});
|
||||
expect(onQueryChange).toHaveBeenCalledTimes(1);
|
||||
expect(onQueryChange.mock.calls[0][0]).toContain("name = 'x' AND updated_at");
|
||||
expect(result.current.reflected.updated).toBe('7d');
|
||||
});
|
||||
|
||||
it('drops the created-by clause and runs when created-by is cleared', () => {
|
||||
const { result, onQueryChange } = setup(
|
||||
"created_by = 'a@x.io' AND name = 'x'",
|
||||
);
|
||||
|
||||
act(() => result.current.handleClearCreatedBy());
|
||||
|
||||
expect(result.current.draft).toBe("name = 'x'");
|
||||
expect(onQueryChange).toHaveBeenCalledWith("name = 'x'");
|
||||
});
|
||||
|
||||
it('empties the draft and runs the empty query on clear', () => {
|
||||
const { result, onQueryChange, onCleared, onClearAll } = setup();
|
||||
|
||||
act(() => result.current.handleClear());
|
||||
|
||||
expect(result.current.draft).toBe('');
|
||||
expect(result.current.isEmpty).toBe(true);
|
||||
expect(onCleared).toHaveBeenCalledTimes(1);
|
||||
expect(onQueryChange).toHaveBeenCalledWith('');
|
||||
expect(onClearAll).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('skips re-running an already-empty query on clear', () => {
|
||||
const { result, onQueryChange, onClearAll } = setup('');
|
||||
|
||||
act(() => result.current.handleClear());
|
||||
|
||||
expect(onQueryChange).not.toHaveBeenCalled();
|
||||
expect(onClearAll).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
15
frontend/src/components/ListFilter/FilterZone/constants.ts
Normal file
15
frontend/src/components/ListFilter/FilterZone/constants.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { UpdatedWindow } from '../dsl/types';
|
||||
|
||||
const UPDATED_LABELS: Record<UpdatedWindow, string> = {
|
||||
any: 'Any time',
|
||||
today: 'Today',
|
||||
'7d': 'Last 7 days',
|
||||
'30d': 'Last 30 days',
|
||||
};
|
||||
|
||||
const UPDATED_WINDOWS: UpdatedWindow[] = ['any', 'today', '7d', '30d'];
|
||||
|
||||
export const UPDATED_OPTIONS = UPDATED_WINDOWS.map((w) => ({
|
||||
value: w,
|
||||
label: UPDATED_LABELS[w],
|
||||
}));
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
createdByClause,
|
||||
parseReflectedClauses,
|
||||
type ReflectedClauses,
|
||||
spliceClause,
|
||||
updatedClause,
|
||||
} from '../dsl/filterQuery';
|
||||
import type { UpdatedWindow } from '../dsl/types';
|
||||
import type { FilterAppliedPayload, FilterAppliedType } from '../types';
|
||||
|
||||
interface Args {
|
||||
query: string;
|
||||
onQueryChange: (value: string) => void;
|
||||
onFilterApplied?: (
|
||||
filterType: FilterAppliedType,
|
||||
payload: FilterAppliedPayload,
|
||||
) => void;
|
||||
onCleared?: () => void;
|
||||
onClearAll?: () => void;
|
||||
}
|
||||
|
||||
interface FilterZoneDraft {
|
||||
draft: string;
|
||||
setDraft: (value: string) => void;
|
||||
reflected: ReflectedClauses;
|
||||
dirty: boolean;
|
||||
isEmpty: boolean;
|
||||
run: () => void;
|
||||
handleCreatedByChange: (emails: string[]) => void;
|
||||
handleUpdatedChange: (window: UpdatedWindow) => void;
|
||||
handleClearCreatedBy: () => void;
|
||||
handleClear: () => void;
|
||||
}
|
||||
|
||||
// The query box is a DRAFT: typing and the Created-by / Updated dropdowns all edit
|
||||
// the draft (dropdowns splice their clause in); nothing fetches until the draft is
|
||||
// run. The draft re-syncs when the last-run query changes externally (view
|
||||
// select, back/forward).
|
||||
export function useFilterZoneDraft({
|
||||
query,
|
||||
onQueryChange,
|
||||
onFilterApplied,
|
||||
onCleared,
|
||||
onClearAll,
|
||||
}: Args): FilterZoneDraft {
|
||||
const [draft, setDraft] = useState(query);
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(query);
|
||||
}, [query]);
|
||||
|
||||
const reflected = useMemo(() => parseReflectedClauses(draft), [draft]);
|
||||
const dirty = draft.trim() !== query.trim();
|
||||
const isEmpty = !draft.trim();
|
||||
|
||||
const commit = useCallback(
|
||||
(next: string): void => {
|
||||
const trimmed = next.trim();
|
||||
if (trimmed !== query) {
|
||||
onQueryChange(trimmed);
|
||||
}
|
||||
},
|
||||
[query, onQueryChange],
|
||||
);
|
||||
|
||||
const run = useCallback((): void => commit(draft), [commit, draft]);
|
||||
|
||||
// Created-by (multi-select) only STAGES its clause into the draft; the query runs
|
||||
// when the dropdown closes (FilterChips fires onApply), not on each pick.
|
||||
const handleCreatedByChange = useCallback(
|
||||
(emails: string[]): void => {
|
||||
onFilterApplied?.('createdBy', { valueCount: emails.length });
|
||||
setDraft((d) => spliceClause(d, 'created_by', createdByClause(emails)));
|
||||
},
|
||||
[onFilterApplied],
|
||||
);
|
||||
|
||||
// Updated (single-select) is a definitive single choice, so it splices AND runs
|
||||
// immediately (with the fresh value, avoiding the async draft-state lag).
|
||||
const handleUpdatedChange = useCallback(
|
||||
(window: UpdatedWindow): void => {
|
||||
onFilterApplied?.('updatedWindow', {});
|
||||
const next = spliceClause(draft, 'updated_at', updatedClause(window));
|
||||
setDraft(next);
|
||||
commit(next);
|
||||
},
|
||||
[draft, commit, onFilterApplied],
|
||||
);
|
||||
|
||||
// Clear-all on the Created-by select removes its clause and runs immediately.
|
||||
const handleClearCreatedBy = useCallback((): void => {
|
||||
const next = spliceClause(draft, 'created_by', null);
|
||||
setDraft(next);
|
||||
commit(next);
|
||||
}, [draft, commit]);
|
||||
|
||||
// Clear resets the draft and runs immediately (empty query).
|
||||
const handleClear = useCallback((): void => {
|
||||
onCleared?.();
|
||||
setDraft('');
|
||||
if (query !== '') {
|
||||
onQueryChange('');
|
||||
}
|
||||
onClearAll?.();
|
||||
}, [query, onQueryChange, onCleared, onClearAll]);
|
||||
|
||||
return {
|
||||
draft,
|
||||
setDraft,
|
||||
reflected,
|
||||
dirty,
|
||||
isEmpty,
|
||||
run,
|
||||
handleCreatedByChange,
|
||||
handleUpdatedChange,
|
||||
handleClearCreatedBy,
|
||||
handleClear,
|
||||
};
|
||||
}
|
||||
@@ -165,8 +165,9 @@
|
||||
|
||||
.leadIcon {
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
top: 50%;
|
||||
left: 10px;
|
||||
transform: translateY(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -176,8 +177,10 @@
|
||||
|
||||
.submit {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
top: 50%;
|
||||
display: flex;
|
||||
right: 6px;
|
||||
transform: translateY(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -15,18 +15,20 @@ import CodeMirror, {
|
||||
Prec,
|
||||
type ReactCodeMirrorRef,
|
||||
} from '@uiw/react-codemirror';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { DashboardListEvents } from 'pages/DashboardsListPage/constants/events';
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
|
||||
import type { SuggestionSource } from '../../utils/dslSuggestions';
|
||||
import type { DslGrammar, SuggestionSource } from '../dsl/types';
|
||||
import { dslCompletionSource } from './dslCompletions';
|
||||
import { dslHighlight } from './dslHighlight';
|
||||
import { BASIC_SETUP, EMPTY_SOURCE, editorTheme } from './editorConfig';
|
||||
|
||||
import styles from './SearchBar.module.scss';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
grammar: DslGrammar;
|
||||
// Ids: `${testIdPrefix}`, `-submit`, `-dirty`.
|
||||
testIdPrefix: string;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
placeholder?: string;
|
||||
@@ -35,53 +37,20 @@ interface Props {
|
||||
dirty?: boolean;
|
||||
/** Filtering needs `list`: the draft edits a query that cannot be run. */
|
||||
disabled?: boolean;
|
||||
onSubmitted?: (hasQuery: boolean) => void;
|
||||
}
|
||||
|
||||
const EMPTY_SOURCE: SuggestionSource = {
|
||||
tagKeys: [],
|
||||
tagValuesByKey: {},
|
||||
creatorEmails: [],
|
||||
};
|
||||
|
||||
// Token colours come from `.cm-dsl-*` classes (SearchBar.module.scss); this only
|
||||
// handles layout — transparent background, monospace, single line.
|
||||
const editorTheme = EditorView.theme({
|
||||
'&': { fontSize: '13px', backgroundColor: 'transparent' },
|
||||
'&.cm-focused': { outline: 'none' },
|
||||
'.cm-content': {
|
||||
// Leave room for the overlaid lead icon (left) and Run button (right).
|
||||
padding: '8px 120px 8px 30px',
|
||||
fontFamily: "'Space Mono', monospace",
|
||||
color: 'var(--l1-foreground)',
|
||||
caretColor: 'var(--l1-foreground)',
|
||||
},
|
||||
'.cm-line': { padding: '0' },
|
||||
'.cm-scroller': { fontFamily: 'inherit', lineHeight: '1.5' },
|
||||
'.cm-placeholder': { color: 'var(--l3-foreground)' },
|
||||
});
|
||||
|
||||
const BASIC_SETUP = {
|
||||
lineNumbers: false,
|
||||
foldGutter: false,
|
||||
highlightActiveLine: false,
|
||||
highlightActiveLineGutter: false,
|
||||
highlightSelectionMatches: false,
|
||||
bracketMatching: false,
|
||||
closeBrackets: false,
|
||||
autocompletion: false,
|
||||
// Use the browser's native caret so it blinks (a CM-drawn cursor needs extra
|
||||
// styling and doesn't blink by default).
|
||||
drawSelection: false,
|
||||
} as const;
|
||||
|
||||
function SearchBar({
|
||||
value,
|
||||
grammar,
|
||||
testIdPrefix,
|
||||
onChange,
|
||||
onSubmit,
|
||||
placeholder = "Filter with DSL (e.g. name CONTAINS 'foo')",
|
||||
source = EMPTY_SOURCE,
|
||||
dirty = false,
|
||||
disabled = false,
|
||||
onSubmitted,
|
||||
}: Props): JSX.Element {
|
||||
const isMac = getUserOperatingSystem() === UserOperatingSystem.MACOS;
|
||||
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
||||
@@ -89,12 +58,12 @@ function SearchBar({
|
||||
// Refs so the (memoised, stable) extensions always see the latest values.
|
||||
const sourceRef = useRef(source);
|
||||
sourceRef.current = source;
|
||||
const grammarRef = useRef(grammar);
|
||||
grammarRef.current = grammar;
|
||||
const handleSubmit = useCallback((): void => {
|
||||
void logEvent(DashboardListEvents.SearchExecuted, {
|
||||
hasQuery: !!value.trim(),
|
||||
});
|
||||
onSubmitted?.(!!value.trim());
|
||||
onSubmit();
|
||||
}, [onSubmit, value]);
|
||||
}, [onSubmit, onSubmitted, value]);
|
||||
const onSubmitRef = useRef(handleSubmit);
|
||||
onSubmitRef.current = handleSubmit;
|
||||
|
||||
@@ -110,7 +79,12 @@ function SearchBar({
|
||||
? []
|
||||
: [
|
||||
autocompletion({
|
||||
override: [dslCompletionSource(() => sourceRef.current)],
|
||||
override: [
|
||||
dslCompletionSource(
|
||||
() => sourceRef.current,
|
||||
() => grammarRef.current,
|
||||
),
|
||||
],
|
||||
closeOnBlur: true,
|
||||
activateOnTyping: true,
|
||||
maxRenderedOptions: 100,
|
||||
@@ -168,7 +142,7 @@ function SearchBar({
|
||||
basicSetup={BASIC_SETUP}
|
||||
indentWithTab={false}
|
||||
editable={!disabled}
|
||||
data-testid="dashboards-list-search"
|
||||
data-testid={testIdPrefix}
|
||||
onChange={(next): void => onChange(next.replace(/\n/g, ' '))}
|
||||
/>
|
||||
<span
|
||||
@@ -182,13 +156,13 @@ function SearchBar({
|
||||
size="sm"
|
||||
aria-label="Run search"
|
||||
disabled={disabled}
|
||||
testId="dashboards-list-search-submit"
|
||||
testId={`${testIdPrefix}-submit`}
|
||||
onClick={handleSubmit}
|
||||
prefix={
|
||||
dirty ? (
|
||||
<span
|
||||
className={styles.dirtyDot}
|
||||
data-testid="dashboards-list-search-dirty"
|
||||
data-testid={`${testIdPrefix}-dirty`}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
@@ -7,18 +7,21 @@ import {
|
||||
} from '@codemirror/autocomplete';
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
|
||||
import {
|
||||
getSuggestions,
|
||||
type SuggestionSource,
|
||||
} from '../../utils/dslSuggestions';
|
||||
import { getSuggestions } from '../dsl/dslSuggestions';
|
||||
import type { DslGrammar, SuggestionSource } from '../dsl/types';
|
||||
|
||||
// `getSource` is a getter so the freshest tags/users (which load async) are read
|
||||
// on every keystroke.
|
||||
// Getters so the freshest keys/users (which load async) are read on every
|
||||
// keystroke.
|
||||
export const dslCompletionSource =
|
||||
(getSource: () => SuggestionSource) =>
|
||||
(getSource: () => SuggestionSource, getGrammar: () => DslGrammar) =>
|
||||
(context: CompletionContext): CompletionResult | null => {
|
||||
const query = context.state.doc.toString();
|
||||
const { items, ctx } = getSuggestions(query, context.pos, getSource());
|
||||
const { items, ctx } = getSuggestions(
|
||||
query,
|
||||
context.pos,
|
||||
getSource(),
|
||||
getGrammar(),
|
||||
);
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// CodeMirror syntax highlighting for the dashboards-list DSL, driven by the same
|
||||
// CodeMirror syntax highlighting for the list-filter DSL, driven by the same
|
||||
// tokenizer used for autocomplete (dslTokenizer). Each key / operator / value /
|
||||
// AND-OR span gets a `cm-dsl-*` class coloured in SearchBar.module.scss.
|
||||
import { RangeSetBuilder } from '@codemirror/state';
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type ViewUpdate,
|
||||
} from '@codemirror/view';
|
||||
|
||||
import { scanTerm, splitTopLevelTerms } from '../../utils/dslTokenizer';
|
||||
import { scanTerm, splitTopLevelTerms } from '../dsl/dslTokenizer';
|
||||
|
||||
const KEY = Decoration.mark({ class: 'cm-dsl-key' });
|
||||
const OP = Decoration.mark({ class: 'cm-dsl-op' });
|
||||
41
frontend/src/components/ListFilter/SearchBar/editorConfig.ts
Normal file
41
frontend/src/components/ListFilter/SearchBar/editorConfig.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { EditorView } from '@uiw/react-codemirror';
|
||||
|
||||
import type { SuggestionSource } from '../dsl/types';
|
||||
|
||||
export const EMPTY_SOURCE: SuggestionSource = {
|
||||
dynamicKeys: [],
|
||||
valuesByKey: {},
|
||||
creatorEmails: [],
|
||||
};
|
||||
|
||||
// Token colours come from `.cm-dsl-*` classes (SearchBar.module.scss); this only
|
||||
// handles layout — transparent background, monospace, single line.
|
||||
export const editorTheme = EditorView.theme({
|
||||
'&': { fontSize: '13px', backgroundColor: 'transparent' },
|
||||
'&.cm-focused': { outline: 'none' },
|
||||
'.cm-content': {
|
||||
// Leave room for the overlaid lead icon (left) and Run button (right).
|
||||
padding: '8px 120px 8px 30px',
|
||||
fontFamily: "'Space Mono', monospace",
|
||||
color: 'var(--l1-foreground)',
|
||||
caretColor: 'var(--l1-foreground)',
|
||||
},
|
||||
'.cm-line': { padding: '0' },
|
||||
// Whole pixels: at 19.5px the placeholder line rounds 1px taller than text.
|
||||
'.cm-scroller': { fontFamily: 'inherit', lineHeight: '20px' },
|
||||
'.cm-placeholder': { color: 'var(--l3-foreground)' },
|
||||
});
|
||||
|
||||
export const BASIC_SETUP = {
|
||||
lineNumbers: false,
|
||||
foldGutter: false,
|
||||
highlightActiveLine: false,
|
||||
highlightActiveLineGutter: false,
|
||||
highlightSelectionMatches: false,
|
||||
bracketMatching: false,
|
||||
closeBrackets: false,
|
||||
autocompletion: false,
|
||||
// Use the browser's native caret so it blinks (a CM-drawn cursor needs extra
|
||||
// styling and doesn't blink by default).
|
||||
drawSelection: false,
|
||||
} as const;
|
||||
@@ -1,36 +1,21 @@
|
||||
// Frontend mirror of the backend dashboards-list filter DSL grammar
|
||||
// (pkg/types/dashboardtypes/list_filter.go). There is no keys/operators API, so
|
||||
// the valid keys, per-field operators, and value shapes are encoded here. Keep in
|
||||
// sync with the backend allow-lists.
|
||||
// Grammar-independent operator tables for the list-filter DSL. Key sets and
|
||||
// field types come from a DslGrammar config mirroring the backend allow-list
|
||||
// (e.g. pkg/types/dashboardtypes/list_filter.go); keep those configs in sync.
|
||||
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
|
||||
|
||||
// Reserved keys the backend recognises as dashboard columns. Any other key is a
|
||||
// tag key. (`updated_by` and `source` are NOT queryable.)
|
||||
export const RESERVED_KEYS = [
|
||||
'name',
|
||||
'description',
|
||||
'created_by',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'locked',
|
||||
] as const;
|
||||
import type { DslField, DslFieldType, DslGrammar } from './types';
|
||||
|
||||
export type DslFieldType = 'string' | 'timestamp' | 'bool' | 'tag';
|
||||
export const findField = (
|
||||
grammar: DslGrammar,
|
||||
key: string,
|
||||
): DslField | undefined =>
|
||||
grammar.fields.find((f) => f.key === key.toLowerCase());
|
||||
|
||||
const RESERVED_FIELD_TYPES: Record<string, DslFieldType> = {
|
||||
name: 'string',
|
||||
description: 'string',
|
||||
created_by: 'string',
|
||||
created_at: 'timestamp',
|
||||
updated_at: 'timestamp',
|
||||
locked: 'bool',
|
||||
};
|
||||
// A reserved key maps to its column type; anything else is dynamic.
|
||||
export const classifyField = (grammar: DslGrammar, key: string): DslFieldType =>
|
||||
findField(grammar, key)?.type ?? 'dynamic';
|
||||
|
||||
// A reserved key maps to its column type; anything else is a tag.
|
||||
export const classifyField = (key: string): DslFieldType =>
|
||||
RESERVED_FIELD_TYPES[key.toLowerCase()] ?? 'tag';
|
||||
|
||||
// String/tag comparison operators, reusing the shared operator constants
|
||||
// String/dynamic comparison operators, reusing the shared operator constants
|
||||
// (REGEXP is allow-listed by the backend but unimplemented, so it is omitted).
|
||||
const STRING_OPS = [
|
||||
OPERATORS['='],
|
||||
@@ -45,10 +30,10 @@ const STRING_OPS = [
|
||||
negateOperator(OPERATORS.IN),
|
||||
];
|
||||
|
||||
// Valid operators per field type. Tags additionally support existence checks.
|
||||
// Valid operators per field type. Dynamic keys additionally support existence checks.
|
||||
export const OPERATOR_MATRIX: Record<DslFieldType, string[]> = {
|
||||
string: STRING_OPS,
|
||||
tag: [...STRING_OPS, OPERATORS.EXISTS, negateOperator(OPERATORS.EXISTS)],
|
||||
dynamic: [...STRING_OPS, OPERATORS.EXISTS, negateOperator(OPERATORS.EXISTS)],
|
||||
timestamp: [
|
||||
OPERATORS['='],
|
||||
OPERATORS['!='],
|
||||
@@ -60,6 +45,12 @@ export const OPERATOR_MATRIX: Record<DslFieldType, string[]> = {
|
||||
negateOperator(OPERATORS.BETWEEN),
|
||||
],
|
||||
bool: [OPERATORS['='], OPERATORS['!=']],
|
||||
enum: [
|
||||
OPERATORS['='],
|
||||
OPERATORS['!='],
|
||||
OPERATORS.IN,
|
||||
negateOperator(OPERATORS.IN),
|
||||
],
|
||||
};
|
||||
|
||||
// Operators that take no value (the clause is complete after the operator).
|
||||
@@ -1,16 +1,23 @@
|
||||
import { getSuggestions, type SuggestionSource } from './dslSuggestions';
|
||||
import { DASHBOARDS_DSL_GRAMMAR } from 'pages/DashboardsListPage/list.config';
|
||||
|
||||
import { getSuggestions } from './dslSuggestions';
|
||||
import type { SuggestionSource } from './types';
|
||||
|
||||
const source: SuggestionSource = {
|
||||
tagKeys: ['env', 'team'],
|
||||
tagValuesByKey: { env: ['prod', 'dev'] },
|
||||
dynamicKeys: ['env', 'team'],
|
||||
valuesByKey: { env: ['prod', 'dev'] },
|
||||
creatorEmails: ['alice@x.io', 'bob@x.io'],
|
||||
currentUserEmail: 'me@x.io',
|
||||
};
|
||||
|
||||
const labels = (q: string, caret: number): string[] =>
|
||||
getSuggestions(q, caret, source).items.map((i) => i.label);
|
||||
getSuggestions(q, caret, source, DASHBOARDS_DSL_GRAMMAR).items.map(
|
||||
(i) => i.label,
|
||||
);
|
||||
const inserts = (q: string, caret: number): string[] =>
|
||||
getSuggestions(q, caret, source).items.map((i) => i.insertText);
|
||||
getSuggestions(q, caret, source, DASHBOARDS_DSL_GRAMMAR).items.map(
|
||||
(i) => i.insertText,
|
||||
);
|
||||
|
||||
describe('getSuggestions — key stage', () => {
|
||||
it('suggests reserved + tag keys matching the partial', () => {
|
||||
@@ -61,7 +68,12 @@ describe('getSuggestions — operator stage', () => {
|
||||
|
||||
describe('getSuggestions — value stage', () => {
|
||||
it('suggests creator emails (current user first, labelled) for created_by', () => {
|
||||
const out = getSuggestions('created_by = ', 13, source);
|
||||
const out = getSuggestions(
|
||||
'created_by = ',
|
||||
13,
|
||||
source,
|
||||
DASHBOARDS_DSL_GRAMMAR,
|
||||
);
|
||||
expect(out.items[0].label).toBe('me@x.io (me)');
|
||||
expect(out.items.map((i) => i.insertText)).toContain("'alice@x.io' ");
|
||||
});
|
||||
@@ -86,7 +98,7 @@ describe('getSuggestions — value stage', () => {
|
||||
describe('getSuggestions — IN list values', () => {
|
||||
it('wraps a fresh IN value in a bracketed list, caret before the close', () => {
|
||||
const q = 'created_by IN ';
|
||||
const out = getSuggestions(q, q.length, source);
|
||||
const out = getSuggestions(q, q.length, source, DASHBOARDS_DSL_GRAMMAR);
|
||||
const alice = out.items.find((i) => i.label === 'alice@x.io');
|
||||
expect(alice?.insertText).toBe("['alice@x.io']");
|
||||
expect(alice?.caretOffset).toBe(1);
|
||||
@@ -100,7 +112,7 @@ describe('getSuggestions — IN list values', () => {
|
||||
it('appends to an open list and excludes already-entered values', () => {
|
||||
const q = "env IN ['prod', ]";
|
||||
const caret = q.indexOf(']'); // inside the brackets, after "'prod', "
|
||||
const out = getSuggestions(q, caret, source);
|
||||
const out = getSuggestions(q, caret, source, DASHBOARDS_DSL_GRAMMAR);
|
||||
expect(out.items.map((i) => i.label)).toStrictEqual(['dev']);
|
||||
const dev = out.items[0];
|
||||
// No closing bracket (the existing `]` stays); caret lands at the end.
|
||||
@@ -110,7 +122,7 @@ describe('getSuggestions — IN list values', () => {
|
||||
|
||||
it('treats a complete last literal (no trailing comma) as entered', () => {
|
||||
const q = "env IN ['prod'";
|
||||
const out = getSuggestions(q, q.length, source);
|
||||
const out = getSuggestions(q, q.length, source, DASHBOARDS_DSL_GRAMMAR);
|
||||
// 'prod' is committed; only 'dev' remains and it appends after it.
|
||||
expect(out.items.map((i) => i.label)).toStrictEqual(['dev']);
|
||||
expect(out.items[0].insertText).toBe("['prod', 'dev'");
|
||||
@@ -1,48 +1,29 @@
|
||||
// Stage-aware autocomplete for the dashboards-list DSL: as the caret moves, it
|
||||
// Stage-aware autocomplete for the list-filter DSL: as the caret moves, it
|
||||
// suggests keys, then operators (valid for that key's field type), then values
|
||||
// (drawn from the tags/users the API reports). Grammar lives in dslGrammar.ts;
|
||||
// caret/stage detection in dslTokenizer.ts.
|
||||
// (drawn from the dynamic keys/users the API reports). Operator tables live in
|
||||
// dslGrammar.ts; caret/stage detection in dslTokenizer.ts.
|
||||
import {
|
||||
classifyField,
|
||||
findField,
|
||||
LIST_OPERATORS,
|
||||
literal,
|
||||
OPERATOR_MATRIX,
|
||||
RESERVED_KEYS,
|
||||
VALUELESS_OPERATORS,
|
||||
} from './dslGrammar';
|
||||
import { type CaretContext, getCaretContext } from './dslTokenizer';
|
||||
import type {
|
||||
DslGrammar,
|
||||
Suggestion,
|
||||
SuggestionSource,
|
||||
SuggestionsResult,
|
||||
} from './types';
|
||||
|
||||
// Show the full key set (the popup scrolls); values are capped a bit higher too.
|
||||
const KEY_LIMIT = 100;
|
||||
const VALUE_LIMIT = 50;
|
||||
|
||||
export interface SuggestionSource {
|
||||
// Reserved column keys (from the list response, falling back to RESERVED_KEYS).
|
||||
reservedKeys?: string[];
|
||||
tagKeys: string[];
|
||||
// Known values per tag key (lower-cased key → values).
|
||||
tagValuesByKey: Record<string, string[]>;
|
||||
// Creator emails for `created_by` value suggestions.
|
||||
creatorEmails: string[];
|
||||
currentUserEmail?: string;
|
||||
}
|
||||
|
||||
export interface Suggestion {
|
||||
label: string;
|
||||
insertText: string;
|
||||
kind: 'key' | 'operator' | 'value' | 'connector';
|
||||
// Right-aligned hint in the popup — e.g. 'field' vs 'tag' so reserved columns
|
||||
// are distinguishable from org tag keys.
|
||||
detail?: string;
|
||||
// Chars to move the caret back from the end of `insertText` after applying.
|
||||
// Used to land the caret inside a `[...]` list so multi-select can continue.
|
||||
caretOffset?: number;
|
||||
}
|
||||
|
||||
export interface SuggestionsResult {
|
||||
items: Suggestion[];
|
||||
ctx: CaretContext;
|
||||
}
|
||||
// Reserved keys whose values are user emails.
|
||||
const USER_KEYS = new Set(['created_by', 'updated_by']);
|
||||
|
||||
const includesInsensitive = (haystack: string, needle: string): boolean =>
|
||||
haystack.toLowerCase().includes(needle.toLowerCase());
|
||||
@@ -52,17 +33,20 @@ const dedupe = (values: string[]): string[] => Array.from(new Set(values));
|
||||
const keySuggestions = (
|
||||
partial: string,
|
||||
source: SuggestionSource,
|
||||
grammar: DslGrammar,
|
||||
): Suggestion[] => {
|
||||
const reserved = source.reservedKeys ?? [...RESERVED_KEYS];
|
||||
const hidden = new Set(grammar.hiddenKeys ?? []);
|
||||
const known = grammar.fields.map((f) => f.key);
|
||||
const reserved = (source.reservedKeys ?? known).filter((k) => !hidden.has(k));
|
||||
const reservedSet = new Set(reserved.map((k) => k.toLowerCase()));
|
||||
// Show reserved keys in our defined (purpose) order — name, description, … —
|
||||
// rather than the API's alphabetical order; any extra API keys follow, then tags.
|
||||
const known = RESERVED_KEYS as readonly string[];
|
||||
// Show reserved keys in the grammar's (purpose) order — name, description, … —
|
||||
// rather than the API's alphabetical order; any extra API keys follow, then
|
||||
// dynamic keys.
|
||||
const orderedReserved = [
|
||||
...known.filter((k) => reserved.includes(k)),
|
||||
...reserved.filter((k) => !known.includes(k)),
|
||||
];
|
||||
const keys = dedupe([...orderedReserved, ...source.tagKeys]);
|
||||
const keys = dedupe([...orderedReserved, ...source.dynamicKeys]);
|
||||
const lower = partial.toLowerCase();
|
||||
return keys
|
||||
.filter((k) => includesInsensitive(k, partial) && k.toLowerCase() !== lower)
|
||||
@@ -71,16 +55,19 @@ const keySuggestions = (
|
||||
label: k,
|
||||
insertText: `${k} `,
|
||||
kind: 'key',
|
||||
// Distinguish built-in columns from org tag keys in the popup.
|
||||
detail: reservedSet.has(k.toLowerCase()) ? 'field' : 'tag',
|
||||
// Distinguish built-in columns from dynamic keys in the popup.
|
||||
detail: reservedSet.has(k.toLowerCase())
|
||||
? 'field'
|
||||
: grammar.dynamicKey.detail,
|
||||
}));
|
||||
};
|
||||
|
||||
const operatorSuggestions = (
|
||||
fieldKey: string,
|
||||
partial: string,
|
||||
grammar: DslGrammar,
|
||||
): Suggestion[] => {
|
||||
const ops = OPERATOR_MATRIX[classifyField(fieldKey)];
|
||||
const ops = OPERATOR_MATRIX[classifyField(grammar, fieldKey)];
|
||||
const upper = partial.toUpperCase();
|
||||
return ops
|
||||
.filter((op) => op.startsWith(upper))
|
||||
@@ -155,15 +142,39 @@ const parseListPartial = (partial: string): ListPartial => {
|
||||
};
|
||||
};
|
||||
|
||||
// Known values for a key: user emails, enum values, or the dynamic key's values
|
||||
// (a reserved dynamic key falls back to its prefixed alias, e.g. `labels.severity`).
|
||||
const knownValues = (
|
||||
key: string,
|
||||
source: SuggestionSource,
|
||||
grammar: DslGrammar,
|
||||
): string[] | null => {
|
||||
const type = classifyField(grammar, key);
|
||||
if (USER_KEYS.has(key) && type === 'string') {
|
||||
return source.currentUserEmail
|
||||
? dedupe([source.currentUserEmail, ...source.creatorEmails])
|
||||
: source.creatorEmails;
|
||||
}
|
||||
if (type === 'enum') {
|
||||
return [...(findField(grammar, key)?.values ?? [])];
|
||||
}
|
||||
if (type === 'dynamic') {
|
||||
const alias = `${grammar.dynamicKey.prefix}${key}`.toLowerCase();
|
||||
return source.valuesByKey[key] ?? source.valuesByKey[alias] ?? [];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const valueSuggestions = (
|
||||
ctx: CaretContext,
|
||||
source: SuggestionSource,
|
||||
grammar: DslGrammar,
|
||||
): Suggestion[] => {
|
||||
if (VALUELESS_OPERATORS.has(ctx.operator)) {
|
||||
return [];
|
||||
}
|
||||
const key = ctx.fieldKey.toLowerCase();
|
||||
const type = classifyField(ctx.fieldKey);
|
||||
const type = classifyField(grammar, ctx.fieldKey);
|
||||
|
||||
// List operators (`IN`/`NOT IN`) take a `[...]` list: parse what's already in
|
||||
// the brackets so we exclude entered values and append rather than replace.
|
||||
@@ -180,15 +191,8 @@ const valueSuggestions = (
|
||||
.map((v) => ({ label: v, insertText: `${v} `, kind: 'value' }));
|
||||
}
|
||||
|
||||
let raw: string[] = [];
|
||||
if (key === 'created_by') {
|
||||
const emails = source.currentUserEmail
|
||||
? dedupe([source.currentUserEmail, ...source.creatorEmails])
|
||||
: source.creatorEmails;
|
||||
raw = emails;
|
||||
} else if (type === 'tag') {
|
||||
raw = source.tagValuesByKey[key] ?? [];
|
||||
} else {
|
||||
const raw = knownValues(key, source, grammar);
|
||||
if (raw === null) {
|
||||
// name / description / timestamps have no known value set — free text.
|
||||
return [];
|
||||
}
|
||||
@@ -199,7 +203,7 @@ const valueSuggestions = (
|
||||
.slice(0, VALUE_LIMIT)
|
||||
.map((v) => {
|
||||
const label =
|
||||
v === source.currentUserEmail && key === 'created_by' ? `${v} (me)` : v;
|
||||
v === source.currentUserEmail && USER_KEYS.has(key) ? `${v} (me)` : v;
|
||||
if (isList) {
|
||||
const values = [...(list?.committed ?? []), v].map(literal).join(', ');
|
||||
return {
|
||||
@@ -239,15 +243,16 @@ export const getSuggestions = (
|
||||
query: string,
|
||||
caret: number,
|
||||
source: SuggestionSource,
|
||||
grammar: DslGrammar,
|
||||
): SuggestionsResult => {
|
||||
const ctx = getCaretContext(query, caret);
|
||||
let items: Suggestion[] = [];
|
||||
if (ctx.stage === 'key') {
|
||||
items = keySuggestions(ctx.partial, source);
|
||||
items = keySuggestions(ctx.partial, source, grammar);
|
||||
} else if (ctx.stage === 'operator') {
|
||||
items = operatorSuggestions(ctx.fieldKey, ctx.partial);
|
||||
items = operatorSuggestions(ctx.fieldKey, ctx.partial, grammar);
|
||||
} else if (ctx.stage === 'value') {
|
||||
items = valueSuggestions(ctx, source);
|
||||
items = valueSuggestions(ctx, source, grammar);
|
||||
} else if (ctx.stage === 'connector') {
|
||||
items = connectorSuggestions(ctx.partial);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Quote/paren/bracket-aware scanning of the dashboards-list filter DSL, shared by
|
||||
// Quote/paren/bracket-aware scanning of the list-filter DSL, shared by
|
||||
// the autocomplete engine (dslSuggestions) and the clause splicer (filterQuery).
|
||||
// The DSL is a boolean composition of `key OP value` terms joined by AND/OR.
|
||||
import {
|
||||
@@ -1,5 +1,5 @@
|
||||
// Pure, side-effect-free helpers over the backend list-filter DSL string
|
||||
// (`GET /api/v2/dashboards?query=...`). The query string is the single source of
|
||||
// (e.g. `GET /api/v2/dashboards?query=...`). The query string is the single source of
|
||||
// truth; these build/parse/splice clauses for it. Free of React so the same
|
||||
// logic backs live filtering, saved views, and built-in views.
|
||||
//
|
||||
@@ -7,11 +7,11 @@
|
||||
// name CONTAINS 'text'
|
||||
// created_by = 'a@b.com' | created_by IN ['a@b.com', 'c@d.com']
|
||||
// updated_at >= '2026-06-01T00:00:00.000Z' (RFC3339)
|
||||
// env = 'prod' (any non-reserved key is a tag filter)
|
||||
// env = 'prod' (any non-reserved key is a dynamic filter, e.g. a tag)
|
||||
// locked = true
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import type { UpdatedWindow } from '../types';
|
||||
import type { ClauseValue, UpdatedWindow } from './types';
|
||||
import { literal } from './dslGrammar';
|
||||
import { scanTerm, splitTopLevelTerms } from './dslTokenizer';
|
||||
|
||||
@@ -37,6 +37,16 @@ export const isQueryEmpty = (query: string): boolean => !query.trim();
|
||||
export const areQueriesEqual = (a: string, b: string): boolean =>
|
||||
a.trim() === b.trim();
|
||||
|
||||
export const clause = (key: string, op: string, value: ClauseValue): string => {
|
||||
if (typeof value === 'boolean') {
|
||||
return `${key} ${op} ${value}`;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return `${key} ${op} ${literal(value)}`;
|
||||
}
|
||||
return `${key} ${op} [${value.map(literal).join(', ')}]`;
|
||||
};
|
||||
|
||||
// The `created_by` clause for a set of emails (single `=`, multiple `IN [...]`).
|
||||
export const createdByClause = (emails: string[]): string | null => {
|
||||
if (emails.length === 0) {
|
||||
@@ -0,0 +1,69 @@
|
||||
import { RULES_DSL_GRAMMAR } from 'container/ListAlertRules/list.config';
|
||||
|
||||
import { getSuggestions } from './dslSuggestions';
|
||||
import type { SuggestionSource } from './types';
|
||||
|
||||
const source: SuggestionSource = {
|
||||
reservedKeys: [
|
||||
'alert_type',
|
||||
'created_at',
|
||||
'created_by',
|
||||
'labels.<key>',
|
||||
'name',
|
||||
'rule_type',
|
||||
'severity',
|
||||
'updated_at',
|
||||
'updated_by',
|
||||
],
|
||||
dynamicKeys: ['labels.team', 'labels.env'],
|
||||
valuesByKey: { 'labels.team': ['infra'], 'labels.severity': ['critical'] },
|
||||
creatorEmails: ['alice@x.io'],
|
||||
currentUserEmail: 'me@x.io',
|
||||
};
|
||||
|
||||
const suggest = (q: string): ReturnType<typeof getSuggestions>['items'] =>
|
||||
getSuggestions(q, q.length, source, RULES_DSL_GRAMMAR).items;
|
||||
const labels = (q: string): string[] => suggest(q).map((i) => i.label);
|
||||
|
||||
describe('getSuggestions — rules grammar', () => {
|
||||
it('orders reserved keys, hides labels.<key> and appends label keys', () => {
|
||||
expect(labels('')).toStrictEqual([
|
||||
'name',
|
||||
'severity',
|
||||
'alert_type',
|
||||
'rule_type',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'labels.team',
|
||||
'labels.env',
|
||||
]);
|
||||
expect(suggest('labels.t').map((i) => i.detail)).toStrictEqual(['label']);
|
||||
});
|
||||
|
||||
it('restricts enum keys to = != IN NOT IN and suggests their values', () => {
|
||||
expect(labels('rule_type ')).toStrictEqual([
|
||||
'rule_type =',
|
||||
'rule_type !=',
|
||||
'rule_type IN',
|
||||
'rule_type NOT IN',
|
||||
]);
|
||||
expect(labels('rule_type = ')).toStrictEqual([
|
||||
'threshold_rule',
|
||||
'promql_rule',
|
||||
'anomaly_rule',
|
||||
]);
|
||||
expect(labels("alert_type = 'LOGS")).toStrictEqual(['LOGS_BASED_ALERT']);
|
||||
});
|
||||
|
||||
it('gives severity and labels.* the label operators and values', () => {
|
||||
expect(labels('severity ')).toContain('severity EXISTS');
|
||||
expect(labels('severity = ')).toStrictEqual(['critical']);
|
||||
expect(labels('labels.team = ')).toStrictEqual(['infra']);
|
||||
});
|
||||
|
||||
it('suggests user emails for updated_by', () => {
|
||||
expect(labels('updated_by = ')).toStrictEqual(['me@x.io (me)', 'alice@x.io']);
|
||||
});
|
||||
});
|
||||
61
frontend/src/components/ListFilter/dsl/types.ts
Normal file
61
frontend/src/components/ListFilter/dsl/types.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { CaretContext } from './dslTokenizer';
|
||||
|
||||
// `dynamic` keys (tags, labels) take string ops plus EXISTS / NOT EXISTS.
|
||||
export type DslFieldType = 'string' | 'timestamp' | 'bool' | 'enum' | 'dynamic';
|
||||
|
||||
export interface DslField {
|
||||
key: string;
|
||||
type: DslFieldType;
|
||||
values?: readonly string[];
|
||||
}
|
||||
|
||||
// A list-filter DSL grammar, mirroring a backend `list_filter.go` allow-list.
|
||||
export interface DslGrammar {
|
||||
// Reserved fields in suggestion order. Any key not listed here is dynamic.
|
||||
fields: readonly DslField[];
|
||||
// `detail` labels dynamic keys in the popup. A reserved dynamic key (e.g.
|
||||
// rules' `severity`) aliases `${prefix}${key}` for value lookups.
|
||||
dynamicKey: { detail: string; prefix: string };
|
||||
// Advertised by the API but not filterable (e.g. `labels.<key>`).
|
||||
hiddenKeys?: readonly string[];
|
||||
}
|
||||
|
||||
export type ClauseValue = string | boolean | readonly string[];
|
||||
|
||||
export type ClauseBuilder<K extends string> = (
|
||||
key: K,
|
||||
op: string,
|
||||
value: ClauseValue,
|
||||
) => string;
|
||||
|
||||
export interface SuggestionSource {
|
||||
// Reserved keys from the list response, falling back to the grammar's.
|
||||
reservedKeys?: string[];
|
||||
// Dynamic keys exactly as typed (e.g. `team`, `labels.team`).
|
||||
dynamicKeys: string[];
|
||||
// Known values per dynamic key (lower-cased full key -> values).
|
||||
valuesByKey: Record<string, string[]>;
|
||||
// Values for `created_by` / `updated_by`.
|
||||
creatorEmails: string[];
|
||||
currentUserEmail?: string;
|
||||
}
|
||||
|
||||
export interface Suggestion {
|
||||
label: string;
|
||||
insertText: string;
|
||||
kind: 'key' | 'operator' | 'value' | 'connector';
|
||||
// Right-aligned hint in the popup — e.g. 'field' vs 'tag' so reserved columns
|
||||
// are distinguishable from dynamic keys.
|
||||
detail?: string;
|
||||
// Chars to move the caret back from the end of `insertText` after applying.
|
||||
// Used to land the caret inside a `[...]` list so multi-select can continue.
|
||||
caretOffset?: number;
|
||||
}
|
||||
|
||||
export interface SuggestionsResult {
|
||||
items: Suggestion[];
|
||||
ctx: CaretContext;
|
||||
}
|
||||
|
||||
// Relative "updated within" windows offered by the Updated filter dropdown.
|
||||
export type UpdatedWindow = 'any' | 'today' | '7d' | '30d';
|
||||
9
frontend/src/components/ListFilter/types.ts
Normal file
9
frontend/src/components/ListFilter/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface CreatorOption {
|
||||
email: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export type FilterAppliedType = 'createdBy' | 'updatedWindow';
|
||||
|
||||
// Analytics payload for a chip change (`valueCount` for Created by).
|
||||
export type FilterAppliedPayload = Record<string, unknown>;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useListUsers } from 'api/generated/services/users';
|
||||
|
||||
import type { CreatorOption } from '../components/FilterZone/FilterChips';
|
||||
import type { CreatorOption } from './types';
|
||||
|
||||
interface Args {
|
||||
currentUserEmail: string;
|
||||
@@ -0,0 +1,46 @@
|
||||
.commandHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.headingBlock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dirtyDot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--warning-background);
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.countPill {
|
||||
padding: 2px 9px;
|
||||
border-radius: 999px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--font-size-xs);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: var(--font-size-lg);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 28px;
|
||||
letter-spacing: -0.09px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1,33 +1,40 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import NewDashboardButton from './NewDashboardButton';
|
||||
|
||||
import styles from './DashboardsList.module.scss';
|
||||
import styles from './ListCommandHeader.module.scss';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
count: number;
|
||||
isModified: boolean;
|
||||
onCreate: () => void;
|
||||
action?: ReactNode;
|
||||
// Ids: `${testIdPrefix}-list-heading`.
|
||||
testIdPrefix?: string;
|
||||
}
|
||||
|
||||
function CommandHeader({
|
||||
function ListCommandHeader({
|
||||
label,
|
||||
count,
|
||||
isModified,
|
||||
onCreate,
|
||||
action,
|
||||
testIdPrefix,
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<div className={styles.commandHeader}>
|
||||
<div className={styles.headingBlock}>
|
||||
<Typography.Title className={styles.title}>{label}</Typography.Title>
|
||||
<Typography.Title
|
||||
className={styles.title}
|
||||
testId={testIdPrefix && `${testIdPrefix}-list-heading`}
|
||||
>
|
||||
{label}
|
||||
</Typography.Title>
|
||||
{isModified && <span className={styles.dirtyDot} title="Unsaved changes" />}
|
||||
<span className={styles.countPill}>{count}</span>
|
||||
</div>
|
||||
<div className={styles.grow} />
|
||||
<NewDashboardButton onClick={onCreate} />
|
||||
{action}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommandHeader;
|
||||
export default ListCommandHeader;
|
||||
@@ -2,20 +2,25 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { PanelLeftClose, PanelLeftOpen } from '@signozhq/icons';
|
||||
|
||||
import styles from './StatusBar.module.scss';
|
||||
import styles from './ListStatusBar.module.scss';
|
||||
|
||||
interface Props {
|
||||
collapsed: boolean;
|
||||
onToggleCollapse: () => void;
|
||||
count: number;
|
||||
total: number;
|
||||
/** Plural noun for the counted items, e.g. "dashboards". */
|
||||
noun: string;
|
||||
testId: string;
|
||||
}
|
||||
|
||||
function StatusBar({
|
||||
function ListStatusBar({
|
||||
collapsed,
|
||||
onToggleCollapse,
|
||||
count,
|
||||
total,
|
||||
noun,
|
||||
testId,
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<div className={styles.statusBar}>
|
||||
@@ -27,15 +32,15 @@ function StatusBar({
|
||||
collapsed ? <PanelLeftOpen size={14} /> : <PanelLeftClose size={14} />
|
||||
}
|
||||
onClick={onToggleCollapse}
|
||||
testId="dashboards-rail-toggle"
|
||||
testId={testId}
|
||||
>
|
||||
{collapsed ? 'Expand' : 'Collapse'}
|
||||
</Button>
|
||||
<Typography.Text className={styles.count}>
|
||||
{count} of {total} dashboards
|
||||
{count} of {total} {noun}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StatusBar;
|
||||
export default ListStatusBar;
|
||||
@@ -9,10 +9,6 @@
|
||||
transition: width 0.18s cubic-bezier(0.2, 0.7, 0.3, 1);
|
||||
}
|
||||
|
||||
.denied {
|
||||
padding: 16px 12px;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
width: 0;
|
||||
border-right-color: transparent;
|
||||
@@ -195,31 +191,3 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.savePopover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 280px;
|
||||
// The popover shell carries no padding of its own, so the form owns it.
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.saveTitle {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.saveLabel {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--l3-foreground);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.saveActions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
264
frontend/src/components/ListViews/ViewsRail/ViewsRail.tsx
Normal file
264
frontend/src/components/ListViews/ViewsRail/ViewsRail.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import {
|
||||
type ChangeEvent,
|
||||
type ReactElement,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Bookmark, Plus, Search } from '@signozhq/icons';
|
||||
import cx from 'classnames';
|
||||
|
||||
import { useDeleteConfirm } from 'components/DeleteConfirmModal/useDeleteConfirm';
|
||||
import ViewNamePopover from 'components/ViewNamePopover/ViewNamePopover';
|
||||
|
||||
import type { BuiltinListView, ViewSaveMode } from '../types';
|
||||
import ViewsRailDirtyPanel from './ViewsRailDirtyPanel';
|
||||
import ViewsRailItem from './ViewsRailItem';
|
||||
|
||||
import styles from './ViewsRail.module.scss';
|
||||
|
||||
interface Props {
|
||||
activeViewId: string;
|
||||
builtinViews: readonly BuiltinListView[];
|
||||
customViews: { id: string; name: string }[];
|
||||
customViewsLoading: boolean;
|
||||
isCustomActive: boolean;
|
||||
isModified: boolean;
|
||||
collapsed?: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
onSave: (name: string) => void;
|
||||
onSaveChanges: () => void;
|
||||
onReset: () => void;
|
||||
onDelete: (id: string) => Promise<void>;
|
||||
onRename: (id: string, name: string) => void;
|
||||
/** Prefixes the rail's test ids, e.g. `<prefix>-view-<id>`. */
|
||||
testIdPrefix: string;
|
||||
saveTestIdPrefix?: string;
|
||||
renameTestIdPrefix?: string;
|
||||
customSectionLabel?: string;
|
||||
deleteDescription?: string;
|
||||
/** Wraps the rail body, e.g. in an authz guard. */
|
||||
guard?: (content: ReactElement) => ReactElement;
|
||||
onViewSaved?: (mode: ViewSaveMode) => void;
|
||||
onViewRenamed?: () => void;
|
||||
onViewDeleted?: () => void;
|
||||
}
|
||||
|
||||
// Purely presentational — active view, dirty state, and handlers come from
|
||||
// `useListViews`.
|
||||
function ViewsRail({
|
||||
activeViewId,
|
||||
builtinViews,
|
||||
customViews,
|
||||
customViewsLoading,
|
||||
isCustomActive,
|
||||
isModified,
|
||||
collapsed = false,
|
||||
onSelect,
|
||||
onSave,
|
||||
onSaveChanges,
|
||||
onReset,
|
||||
onDelete,
|
||||
onRename,
|
||||
testIdPrefix,
|
||||
saveTestIdPrefix = 'save-view',
|
||||
renameTestIdPrefix = 'rename-view',
|
||||
customSectionLabel = 'My views',
|
||||
deleteDescription = 'This removes the saved view.',
|
||||
guard,
|
||||
onViewSaved,
|
||||
onViewRenamed,
|
||||
onViewDeleted,
|
||||
}: Props): JSX.Element {
|
||||
const [saveOpen, setSaveOpen] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
const { contextHolder, confirmDelete } = useDeleteConfirm();
|
||||
|
||||
const q = query.trim().toLowerCase();
|
||||
const matchesQuery = (label: string): boolean =>
|
||||
!q || label.toLowerCase().includes(q);
|
||||
|
||||
const personal = builtinViews.filter(
|
||||
(v) => v.section === 'personal' && matchesQuery(v.label),
|
||||
);
|
||||
const system = builtinViews.filter(
|
||||
(v) => v.section === 'system' && matchesQuery(v.label),
|
||||
);
|
||||
const custom = customViews.filter((v) => matchesQuery(v.name));
|
||||
const noMatches =
|
||||
!!q && personal.length === 0 && system.length === 0 && custom.length === 0;
|
||||
|
||||
const onConfirmDelete = useCallback(
|
||||
(id: string, label: string): void => {
|
||||
confirmDelete({
|
||||
title: (
|
||||
<Typography.Title level={5}>
|
||||
Delete the{' '}
|
||||
<Typography.Text className={styles.deleteName}>{label}</Typography.Text>{' '}
|
||||
view?
|
||||
</Typography.Title>
|
||||
),
|
||||
content: deleteDescription,
|
||||
// Return the delete promise so the modal's Delete button stays loading
|
||||
// until the backend call settles, then closes — matching dashboard delete.
|
||||
onConfirm: (): Promise<void> => {
|
||||
onViewDeleted?.();
|
||||
return onDelete(id);
|
||||
},
|
||||
});
|
||||
},
|
||||
[confirmDelete, deleteDescription, onDelete, onViewDeleted],
|
||||
);
|
||||
|
||||
const handleRename = useCallback(
|
||||
(id: string, name: string): void => {
|
||||
onViewRenamed?.();
|
||||
onRename(id, name);
|
||||
},
|
||||
[onRename, onViewRenamed],
|
||||
);
|
||||
|
||||
const handleSaveAsView = (name: string): void => {
|
||||
onViewSaved?.('new');
|
||||
onSave(name);
|
||||
};
|
||||
|
||||
const handleSaveViewChanges = (): void => {
|
||||
onViewSaved?.('update');
|
||||
onSaveChanges();
|
||||
};
|
||||
|
||||
const renderBuiltin = (v: BuiltinListView): JSX.Element => (
|
||||
<ViewsRailItem
|
||||
key={v.id}
|
||||
id={v.id}
|
||||
label={v.label}
|
||||
icon={v.icon}
|
||||
active={v.id === activeViewId}
|
||||
isModified={isModified}
|
||||
testIdPrefix={testIdPrefix}
|
||||
renameTestIdPrefix={renameTestIdPrefix}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<div className={styles.header}>
|
||||
<h4 className={styles.headerTitle}>Views</h4>
|
||||
<ViewNamePopover
|
||||
open={saveOpen}
|
||||
onOpenChange={setSaveOpen}
|
||||
onSubmit={handleSaveAsView}
|
||||
title="Save as view"
|
||||
confirmLabel="Save view"
|
||||
testIdPrefix={saveTestIdPrefix}
|
||||
tooltip="Save current filters as a view"
|
||||
trigger={
|
||||
<Button
|
||||
aria-label="Save current filters as a view"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon
|
||||
testId={`${testIdPrefix}-view-save-trigger`}
|
||||
>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.search}>
|
||||
<Input
|
||||
value={query}
|
||||
placeholder="Filter views by name"
|
||||
prefix={<Search size={12} />}
|
||||
testId={`${testIdPrefix}-view-search`}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
|
||||
setQuery(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.scroll}>
|
||||
{personal.length > 0 && (
|
||||
<>
|
||||
<div className={styles.groupLabel}>Personal</div>
|
||||
{personal.map(renderBuiltin)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{system.length > 0 && (
|
||||
<>
|
||||
<div className={cx(styles.groupLabel, styles.groupLabelSpaced)}>
|
||||
System
|
||||
</div>
|
||||
{system.map(renderBuiltin)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{(!q || custom.length > 0) && (
|
||||
<>
|
||||
<div className={cx(styles.groupLabel, styles.groupLabelSpaced)}>
|
||||
{customSectionLabel}
|
||||
<Typography.Text className={styles.groupCount}>
|
||||
{customViews.length}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{customViewsLoading ? (
|
||||
<div className={styles.empty}>Loading views…</div>
|
||||
) : customViews.length === 0 ? (
|
||||
<div className={styles.empty}>
|
||||
No saved views yet. Filter the list, then save it as a view.
|
||||
</div>
|
||||
) : (
|
||||
custom.map((v) => (
|
||||
<ViewsRailItem
|
||||
key={v.id}
|
||||
id={v.id}
|
||||
label={v.name}
|
||||
icon={Bookmark}
|
||||
active={v.id === activeViewId}
|
||||
isModified={isModified}
|
||||
testIdPrefix={testIdPrefix}
|
||||
renameTestIdPrefix={renameTestIdPrefix}
|
||||
onSelect={onSelect}
|
||||
onRename={handleRename}
|
||||
onDelete={onConfirmDelete}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{noMatches && (
|
||||
<div className={styles.searchEmpty}>
|
||||
No views match “{query}”
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isModified && (
|
||||
<ViewsRailDirtyPanel
|
||||
isCustomActive={isCustomActive}
|
||||
testIdPrefix={testIdPrefix}
|
||||
onSaveChanges={handleSaveViewChanges}
|
||||
onSaveAs={(): void => setSaveOpen(true)}
|
||||
onReset={onReset}
|
||||
/>
|
||||
)}
|
||||
{contextHolder}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<aside className={cx(styles.rail, { [styles.collapsed]: collapsed })}>
|
||||
{guard ? guard(content) : content}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewsRail;
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import cx from 'classnames';
|
||||
|
||||
import styles from './ViewsRail.module.scss';
|
||||
|
||||
interface Props {
|
||||
isCustomActive: boolean;
|
||||
testIdPrefix: string;
|
||||
onSaveChanges: () => void;
|
||||
onSaveAs: () => void;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
function ViewsRailDirtyPanel({
|
||||
isCustomActive,
|
||||
testIdPrefix,
|
||||
onSaveChanges,
|
||||
onSaveAs,
|
||||
onReset,
|
||||
}: Props): JSX.Element {
|
||||
if (isCustomActive) {
|
||||
return (
|
||||
<div className={styles.dirtyPanel}>
|
||||
<div className={styles.dirtyTitle}>Unsaved changes</div>
|
||||
<div className={styles.dirtyActions}>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="sm"
|
||||
onClick={onSaveChanges}
|
||||
testId={`${testIdPrefix}-view-save-changes`}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={onSaveAs}
|
||||
testId={`${testIdPrefix}-view-save-as`}
|
||||
>
|
||||
Save as…
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={onReset}
|
||||
testId={`${testIdPrefix}-view-reset`}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(styles.dirtyPanel, styles.dirtyPanelDefault)}>
|
||||
<div className={styles.dirtyTitle}>Filters active</div>
|
||||
<div className={styles.dirtyActions}>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="sm"
|
||||
prefix={<Plus size={12} />}
|
||||
onClick={onSaveAs}
|
||||
testId={`${testIdPrefix}-view-save-as-new`}
|
||||
>
|
||||
Save as new view
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={onReset}
|
||||
testId={`${testIdPrefix}-view-reset`}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewsRailDirtyPanel;
|
||||
105
frontend/src/components/ListViews/ViewsRail/ViewsRailItem.tsx
Normal file
105
frontend/src/components/ListViews/ViewsRail/ViewsRailItem.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { PenLine, Trash2 } from '@signozhq/icons';
|
||||
import cx from 'classnames';
|
||||
import ViewNamePopover from 'components/ViewNamePopover/ViewNamePopover';
|
||||
|
||||
import type { ListViewIcon } from '../types';
|
||||
|
||||
import styles from './ViewsRail.module.scss';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: ListViewIcon;
|
||||
active: boolean;
|
||||
isModified: boolean;
|
||||
testIdPrefix: string;
|
||||
renameTestIdPrefix: string;
|
||||
onSelect: (id: string) => void;
|
||||
/** Present only for custom (editable) views. */
|
||||
onRename?: (id: string, name: string) => void;
|
||||
onDelete?: (id: string, label: string) => void;
|
||||
}
|
||||
|
||||
function ViewsRailItem({
|
||||
id,
|
||||
label,
|
||||
icon: Icon,
|
||||
active,
|
||||
isModified,
|
||||
testIdPrefix,
|
||||
renameTestIdPrefix,
|
||||
onSelect,
|
||||
onRename,
|
||||
onDelete,
|
||||
}: Props): JSX.Element {
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={cx(styles.row, { [styles.rowActive]: active })}>
|
||||
<Button
|
||||
size="md"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
width="100%"
|
||||
onClick={(): void => onSelect(id)}
|
||||
testId={`${testIdPrefix}-view-${id}`}
|
||||
prefix={<Icon size={16} className={styles.itemIcon} />}
|
||||
suffix={
|
||||
active && isModified ? (
|
||||
<div className={styles.dirtyDot} title="Unsaved changes" />
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
{onRename && onDelete && (
|
||||
<div className={styles.itemActions}>
|
||||
<ViewNamePopover
|
||||
open={renaming}
|
||||
onOpenChange={setRenaming}
|
||||
onSubmit={(name): void => onRename(id, name)}
|
||||
title="Rename view"
|
||||
confirmLabel="Rename"
|
||||
initialName={label}
|
||||
testIdPrefix={renameTestIdPrefix}
|
||||
tooltip="Rename view"
|
||||
trigger={
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon
|
||||
aria-label="Rename view"
|
||||
testId={`${testIdPrefix}-view-rename-${id}`}
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
>
|
||||
<PenLine size={12} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Tooltip title="Delete view">
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon
|
||||
aria-label="Delete view"
|
||||
testId={`${testIdPrefix}-view-delete-${id}`}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
onDelete(id, label);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewsRailItem;
|
||||
@@ -4,21 +4,21 @@ import { render, screen } from 'tests/test-utils';
|
||||
import { setupAuthzAllow } from 'lib/authz/utils/authz-test-utils';
|
||||
import { DashboardListPermission } from 'lib/authz/hooks/useAuthZ/permissions/dashboard.permissions';
|
||||
|
||||
import type { BuiltinView } from '../../../utils/views';
|
||||
import ViewsRailGuard from 'pages/DashboardsListPage/components/DashboardsList/ViewsRailGuard';
|
||||
|
||||
import type { BuiltinListView } from '../../types';
|
||||
import ViewsRail from '../ViewsRail';
|
||||
|
||||
const BUILTIN_VIEWS: BuiltinView[] = [
|
||||
const BUILTIN_VIEWS: BuiltinListView[] = [
|
||||
{
|
||||
id: 'all',
|
||||
label: 'All dashboards',
|
||||
icon: Bookmark,
|
||||
section: 'system',
|
||||
} as BuiltinView,
|
||||
},
|
||||
];
|
||||
|
||||
const CUSTOM_VIEWS = [
|
||||
{ id: 'view-1', name: 'My prod dashboards', query: '', userEmail: 'a@b.co' },
|
||||
];
|
||||
const CUSTOM_VIEWS = [{ id: 'view-1', name: 'My prod dashboards' }];
|
||||
|
||||
function renderRail(
|
||||
props?: Partial<React.ComponentProps<typeof ViewsRail>>,
|
||||
@@ -27,7 +27,7 @@ function renderRail(
|
||||
<ViewsRail
|
||||
activeViewId="view-1"
|
||||
builtinViews={BUILTIN_VIEWS}
|
||||
customViews={CUSTOM_VIEWS as never}
|
||||
customViews={CUSTOM_VIEWS}
|
||||
customViewsLoading={false}
|
||||
isCustomActive
|
||||
isModified
|
||||
@@ -37,6 +37,8 @@ function renderRail(
|
||||
onReset={jest.fn()}
|
||||
onDelete={jest.fn()}
|
||||
onRename={jest.fn()}
|
||||
testIdPrefix="dashboards"
|
||||
guard={(content): JSX.Element => <ViewsRailGuard>{content}</ViewsRailGuard>}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Bookmark, Pin } from '@signozhq/icons';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import type { BuiltinListView } from '../../types';
|
||||
import ViewsRail from '../ViewsRail';
|
||||
|
||||
const PREFIX = 'things';
|
||||
|
||||
const BUILTIN_VIEWS: BuiltinListView[] = [
|
||||
{ id: 'mine', label: 'My things', icon: Pin, section: 'personal' },
|
||||
{ id: 'all', label: 'All things', icon: Bookmark, section: 'system' },
|
||||
];
|
||||
|
||||
const CUSTOM_VIEWS = [
|
||||
{ id: 'view-1', name: 'Prod things' },
|
||||
{ id: 'view-2', name: 'Staging things' },
|
||||
];
|
||||
|
||||
type Props = React.ComponentProps<typeof ViewsRail>;
|
||||
|
||||
function renderRail(props?: Partial<Props>): Props {
|
||||
const allProps: Props = {
|
||||
activeViewId: 'all',
|
||||
builtinViews: BUILTIN_VIEWS,
|
||||
customViews: CUSTOM_VIEWS,
|
||||
customViewsLoading: false,
|
||||
isCustomActive: false,
|
||||
isModified: false,
|
||||
onSelect: jest.fn(),
|
||||
onSave: jest.fn(),
|
||||
onSaveChanges: jest.fn(),
|
||||
onReset: jest.fn(),
|
||||
onDelete: jest.fn(),
|
||||
onRename: jest.fn(),
|
||||
testIdPrefix: PREFIX,
|
||||
...props,
|
||||
};
|
||||
render(<ViewsRail {...allProps} />);
|
||||
return allProps;
|
||||
}
|
||||
|
||||
describe('ViewsRail', () => {
|
||||
it('groups built-in views by section and custom views under their label', () => {
|
||||
renderRail({ customSectionLabel: 'Team views' });
|
||||
|
||||
expect(screen.getByText('Personal')).toBeInTheDocument();
|
||||
expect(screen.getByText('System')).toBeInTheDocument();
|
||||
expect(screen.getByText('Team views')).toBeInTheDocument();
|
||||
['mine', 'all', 'view-1', 'view-2'].forEach((id) =>
|
||||
expect(screen.getByTestId(`${PREFIX}-view-${id}`)).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('selects a view by id', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const props = renderRail();
|
||||
|
||||
await user.click(screen.getByTestId(`${PREFIX}-view-view-2`));
|
||||
|
||||
expect(props.onSelect).toHaveBeenCalledWith('view-2');
|
||||
});
|
||||
|
||||
it('filters views by name and says when none match', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderRail();
|
||||
const search = screen.getByTestId(`${PREFIX}-view-search`);
|
||||
|
||||
await user.type(search, 'prod');
|
||||
|
||||
expect(screen.getByTestId(`${PREFIX}-view-view-1`)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(`${PREFIX}-view-view-2`)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(`${PREFIX}-view-all`)).not.toBeInTheDocument();
|
||||
|
||||
await user.clear(search);
|
||||
await user.type(search, 'nothing');
|
||||
|
||||
expect(screen.getByText(/No views match/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows loading and empty states for custom views', () => {
|
||||
const { unmount } = render(
|
||||
<ViewsRail
|
||||
activeViewId="all"
|
||||
builtinViews={BUILTIN_VIEWS}
|
||||
customViews={[]}
|
||||
customViewsLoading
|
||||
isCustomActive={false}
|
||||
isModified={false}
|
||||
onSelect={jest.fn()}
|
||||
onSave={jest.fn()}
|
||||
onSaveChanges={jest.fn()}
|
||||
onReset={jest.fn()}
|
||||
onDelete={jest.fn()}
|
||||
onRename={jest.fn()}
|
||||
testIdPrefix={PREFIX}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('Loading views…')).toBeInTheDocument();
|
||||
unmount();
|
||||
|
||||
renderRail({ customViews: [] });
|
||||
expect(screen.getByText(/No saved views yet/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides the unsaved-changes panel while filters match the view', () => {
|
||||
renderRail();
|
||||
|
||||
expect(screen.queryByTestId(`${PREFIX}-view-reset`)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('offers save, save as and reset for a modified custom view', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const onViewSaved = jest.fn();
|
||||
const props = renderRail({
|
||||
activeViewId: 'view-1',
|
||||
isCustomActive: true,
|
||||
isModified: true,
|
||||
onViewSaved,
|
||||
});
|
||||
|
||||
await user.click(screen.getByTestId(`${PREFIX}-view-save-changes`));
|
||||
expect(onViewSaved).toHaveBeenCalledWith('update');
|
||||
expect(props.onSaveChanges).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(screen.getByTestId(`${PREFIX}-view-save-as`)).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByTestId(`${PREFIX}-view-reset`));
|
||||
expect(props.onReset).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('offers only save as new for a modified built-in view', () => {
|
||||
renderRail({ isModified: true });
|
||||
|
||||
expect(screen.getByTestId(`${PREFIX}-view-save-as-new`)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId(`${PREFIX}-view-save-changes`),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('wraps its body in the guard it is given', () => {
|
||||
renderRail({
|
||||
guard: (content): JSX.Element => (
|
||||
<div data-testid="rail-guard">{content}</div>
|
||||
),
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('rail-guard')).toContainElement(
|
||||
screen.getByTestId(`${PREFIX}-view-all`),
|
||||
);
|
||||
});
|
||||
});
|
||||
43
frontend/src/components/ListViews/types.ts
Normal file
43
frontend/src/components/ListViews/types.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { Pin } from '@signozhq/icons';
|
||||
|
||||
// All @signozhq icons share this component type.
|
||||
export type ListViewIcon = typeof Pin;
|
||||
|
||||
export interface BuiltinListView {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: ListViewIcon;
|
||||
section: 'personal' | 'system';
|
||||
}
|
||||
|
||||
export interface EmptyStateCopy {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// `clientFilter` narrows the rows the server already returned.
|
||||
export interface BuiltinViewConfig<
|
||||
F,
|
||||
Ctx = undefined,
|
||||
Row = never,
|
||||
> extends BuiltinListView {
|
||||
filter: (current: F, ctx: Ctx) => F;
|
||||
clientFilter?: (rows: Row[], ctx: Ctx) => Row[];
|
||||
emptyState?: EmptyStateCopy;
|
||||
}
|
||||
|
||||
export interface SavedListView<F> {
|
||||
id: string;
|
||||
name: string;
|
||||
filter: F;
|
||||
}
|
||||
|
||||
export interface SavedListViewsApi<F> {
|
||||
views: SavedListView<F>[];
|
||||
isLoading: boolean;
|
||||
createView: (name: string, filter: F) => Promise<SavedListView<F> | null>;
|
||||
updateView: (id: string, name: string, filter: F) => void;
|
||||
deleteView: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export type ViewSaveMode = 'new' | 'update';
|
||||
@@ -12,6 +12,11 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-family: 'Geist Mono', monospace;
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import {
|
||||
ChevronDown,
|
||||
@@ -76,7 +76,7 @@ function LogDetailsHeader({
|
||||
return (
|
||||
<div className={styles.header} data-log-detail-ignore="true">
|
||||
<div className={styles.leftSection}>
|
||||
<Divider orientation="vertical" height={16} spacing={0} />
|
||||
<Divider type="vertical" className={styles.divider} />
|
||||
<Typography.Text
|
||||
className={styles.timestamp}
|
||||
data-testid="log-details-header-timestamp"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
@@ -7,8 +7,8 @@ import LogDetail from '..';
|
||||
import { VIEW_TYPES } from '../constants';
|
||||
import { LogDetailProps } from '../LogDetail.interfaces';
|
||||
|
||||
jest.mock('@signozhq/ui/toast', () => ({
|
||||
toast: { success: jest.fn(), danger: jest.fn() },
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
// DataViewer pulls in react-json-tree (ESM) + Monaco; mock it (as trace's tests
|
||||
|
||||
@@ -153,36 +153,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// Stepper group: the Input sits seamlessly between the -/+
|
||||
// periscope-btns (26px tall, l1-border).
|
||||
.max-lines-per-row-input {
|
||||
display: flex;
|
||||
|
||||
--input-height: 26px;
|
||||
--input-border-radius: 0;
|
||||
--input-border: var(--l1-border);
|
||||
--input-border-width: 1px 0;
|
||||
|
||||
[data-testid='max-lines-per-row'] {
|
||||
min-width: 36px;
|
||||
width: auto;
|
||||
|
||||
// No Input token for text alignment.
|
||||
input {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.periscope-btn {
|
||||
box-shadow: none;
|
||||
padding: 6px 12px;
|
||||
height: 26px;
|
||||
|
||||
border-radius: 0px 1px 1px 0px;
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-columns-container {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Popover, Tooltip } from 'antd';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { InputNumber } from '@signozhq/ui/input-number';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
LogViewMode,
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
Check,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Minus,
|
||||
Plus,
|
||||
SlidersVertical,
|
||||
} from '@signozhq/icons';
|
||||
|
||||
@@ -182,35 +180,20 @@ function OptionsMenu({
|
||||
<div className="horizontal-line" />
|
||||
<div className="max-lines-per-row">
|
||||
<div className="title"> max lines per row </div>
|
||||
<div className="raw-format max-lines-per-row-input">
|
||||
<button
|
||||
type="button"
|
||||
className="periscope-btn"
|
||||
aria-label="Fewer lines per row"
|
||||
onClick={(): void => handleLinesPerRowChange(maxLinesPerRow - 1)}
|
||||
>
|
||||
<Minus size={12} />
|
||||
</button>
|
||||
<Input.Number
|
||||
min={MIN_LINES_PER_ROW}
|
||||
max={MAX_LINES_PER_ROW}
|
||||
format={{ maximumFractionDigits: 0 }}
|
||||
value={maxLinesPerRow}
|
||||
onChange={handleLinesPerRowChange}
|
||||
controls={false}
|
||||
noFocusRing
|
||||
aria-label="Max lines per row"
|
||||
testId="max-lines-per-row"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="periscope-btn"
|
||||
aria-label="More lines per row"
|
||||
onClick={(): void => handleLinesPerRowChange(maxLinesPerRow + 1)}
|
||||
>
|
||||
<Plus size={12} />
|
||||
</button>
|
||||
</div>
|
||||
<InputNumber
|
||||
controls
|
||||
mode="spinner"
|
||||
size="small"
|
||||
min={MIN_LINES_PER_ROW}
|
||||
max={MAX_LINES_PER_ROW}
|
||||
precision={0}
|
||||
value={maxLinesPerRow}
|
||||
onChange={handleLinesPerRowChange}
|
||||
aria-label="Max lines per row"
|
||||
decrementAriaLabel="Fewer lines per row"
|
||||
incrementAriaLabel="More lines per row"
|
||||
testId="max-lines-per-row"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('LogsFormatOptionsMenu (unit)', () => {
|
||||
const { maxLinesOnChange, getByTestId } = setup();
|
||||
|
||||
// Increment max lines
|
||||
const input = getByTestId('max-lines-per-row-field') as HTMLInputElement;
|
||||
const input = getByTestId('max-lines-per-row') as HTMLInputElement;
|
||||
const initial = Number(input.value);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'More lines per row' }));
|
||||
|
||||
@@ -130,7 +130,7 @@ describe('LogsFormatOptionsMenu (unit)', () => {
|
||||
|
||||
it('clamps a typed max-lines value to 10', async () => {
|
||||
const { maxLinesOnChange, getByTestId } = setup();
|
||||
const input = getByTestId('max-lines-per-row-field') as HTMLInputElement;
|
||||
const input = getByTestId('max-lines-per-row') as HTMLInputElement;
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.clear(input);
|
||||
@@ -143,7 +143,7 @@ describe('LogsFormatOptionsMenu (unit)', () => {
|
||||
|
||||
it('stops max-lines at 10', async () => {
|
||||
const { getByTestId } = setup();
|
||||
const input = getByTestId('max-lines-per-row-field') as HTMLInputElement;
|
||||
const input = getByTestId('max-lines-per-row') as HTMLInputElement;
|
||||
const increment = screen.getByRole('button', { name: 'More lines per row' });
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
@@ -184,8 +184,8 @@
|
||||
}
|
||||
|
||||
.helpContent {
|
||||
// The popover shell carries no padding of its own, so the content owns it.
|
||||
padding: var(--spacing-8);
|
||||
width: 280px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
@include custom-scrollbar;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CircleHelp } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Popover } from '@signozhq/ui/popover';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import { MARKDOWN_HELP_ITEMS } from './constants';
|
||||
@@ -9,14 +9,8 @@ import styles from './MarkdownEditor.module.scss';
|
||||
|
||||
function MarkdownHelp(): JSX.Element {
|
||||
return (
|
||||
<Popover
|
||||
nativeButton
|
||||
side="bottom"
|
||||
align="end"
|
||||
width={280}
|
||||
maxHeight={320}
|
||||
aria-label="Markdown syntax"
|
||||
trigger={
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -28,9 +22,8 @@ function MarkdownHelp(): JSX.Element {
|
||||
>
|
||||
<CircleHelp size={14} />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className={styles.helpContent}>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className={styles.helpContent}>
|
||||
<Typography.Text className={styles.helpTitle}>
|
||||
Markdown syntax
|
||||
</Typography.Text>
|
||||
@@ -44,7 +37,7 @@ function MarkdownHelp(): JSX.Element {
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
.banner {
|
||||
height: var(--spacing-20);
|
||||
|
||||
a {
|
||||
color: var(--callout-warning-title);
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: var(--callout-warning-title);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,25 @@
|
||||
import { AlertStrip } from '@signozhq/ui/alert-strip';
|
||||
import { PersistedAnnouncementBanner } from '@signozhq/ui/announcement-banner';
|
||||
|
||||
import styles from './NoAuthBanner.module.scss';
|
||||
|
||||
export function NoAuthBanner(): JSX.Element {
|
||||
return (
|
||||
<AlertStrip.CloseablePersisted
|
||||
color="warning"
|
||||
side="top"
|
||||
<PersistedAnnouncementBanner
|
||||
type="warning"
|
||||
storageKey="no-auth-banner-v1"
|
||||
testId="no-auth-banner"
|
||||
className={styles.banner}
|
||||
>
|
||||
Impersonation mode: authentication is disabled. Anyone with access to this
|
||||
instance has admin privileges.{' '}
|
||||
<AlertStrip.Link
|
||||
<a
|
||||
href="https://signoz.io/docs/manage/administrator-guide/configuration/impersonation-mode/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more
|
||||
</AlertStrip.Link>
|
||||
</AlertStrip.CloseablePersisted>
|
||||
</a>
|
||||
</PersistedAnnouncementBanner>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,6 @@ describe('NoAuthBanner', () => {
|
||||
render(<NoAuthBanner />);
|
||||
const link = screen.getByRole('link', { name: /learn more/i });
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
|
||||
expect(link).toHaveAttribute('rel', 'noreferrer');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,10 +102,12 @@
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
/** Surface and metrics come from the `--input-*` tokens; the family and tracking are set here. */
|
||||
.numberField {
|
||||
--input-font-family: 'Geist Mono';
|
||||
--input-letter-spacing: -0.07px;
|
||||
/** Surface and metrics come from the `--input-*` tokens above; only the family has none. */
|
||||
.numberInput {
|
||||
width: 104px;
|
||||
|
||||
font-family: 'Geist Mono';
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
/** Unfilled: a pill on the same row as the toggles above reads as another control,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { InputNumber } from '@signozhq/ui/input-number';
|
||||
import { ToggleGroup } from '@signozhq/ui/toggle-group';
|
||||
import { ChevronUp, Info } from '@signozhq/icons';
|
||||
import {
|
||||
@@ -111,17 +111,19 @@ function BucketOptions({
|
||||
);
|
||||
|
||||
const handleMaxValueChange = useCallback(
|
||||
(value: number | null): void => {
|
||||
setMaxValue(value);
|
||||
emitLinear(value, numBuckets);
|
||||
(value: number | string | null): void => {
|
||||
const next = value === null || value === '' ? null : Number(value);
|
||||
setMaxValue(next);
|
||||
emitLinear(next, numBuckets);
|
||||
},
|
||||
[emitLinear, numBuckets],
|
||||
);
|
||||
|
||||
const handleNumBucketsChange = useCallback(
|
||||
(value: number | null): void => {
|
||||
setNumBuckets(value);
|
||||
emitLinear(maxValue, value);
|
||||
(value: number | string | null): void => {
|
||||
const next = value === null || value === '' ? null : Number(value);
|
||||
setNumBuckets(next);
|
||||
emitLinear(maxValue, next);
|
||||
},
|
||||
[emitLinear, maxValue],
|
||||
);
|
||||
@@ -205,29 +207,29 @@ function BucketOptions({
|
||||
|
||||
{kind === Querybuildertypesv5BucketsKindDTO.linear && (
|
||||
<>
|
||||
<div className={cx(styles.field, styles.numberField)}>
|
||||
<div className={styles.field}>
|
||||
<span className={styles.label}>Max value</span>
|
||||
<Input.Number
|
||||
<InputNumber
|
||||
className={styles.numberInput}
|
||||
min={0}
|
||||
value={maxValue}
|
||||
onChange={handleMaxValueChange}
|
||||
placeholder="Required"
|
||||
status={maxValue !== null && maxValue <= 0 ? 'danger' : undefined}
|
||||
width={104}
|
||||
testId="bucket-options-max-value"
|
||||
status={maxValue !== null && maxValue <= 0 ? 'error' : undefined}
|
||||
data-testid="bucket-options-max-value"
|
||||
/>
|
||||
</div>
|
||||
<div className={cx(styles.field, styles.numberField)}>
|
||||
<div className={styles.field}>
|
||||
<span className={styles.label}>Buckets</span>
|
||||
<Input.Number
|
||||
<InputNumber
|
||||
className={styles.numberInput}
|
||||
min={1}
|
||||
max={MAX_NUM_BUCKETS}
|
||||
format={{ maximumFractionDigits: 0 }}
|
||||
precision={0}
|
||||
value={numBuckets}
|
||||
onChange={handleNumBucketsChange}
|
||||
placeholder={`Default ${DEFAULT_NUM_BUCKETS}`}
|
||||
width={104}
|
||||
testId="bucket-options-num-buckets"
|
||||
data-testid="bucket-options-num-buckets"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -65,6 +65,14 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
// Meant to fix the query builder colors
|
||||
--input-background: var(--l2-background);
|
||||
--input-hover-background: var(--l2-background);
|
||||
--input-focus-background: var(--l2-background);
|
||||
--input-border-color: var(--l2-border);
|
||||
--input-hover-border-color: var(--internal-ant-border-color-hover);
|
||||
--input-focus-border-color: var(--internal-ant-border-color-hover);
|
||||
}
|
||||
|
||||
&-aggregation-container {
|
||||
|
||||
@@ -181,7 +181,7 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
|
||||
expect(limitInput).toBeInTheDocument();
|
||||
expect(limitInput).toHaveAttribute('type', 'number');
|
||||
expect(limitInput).toHaveAttribute('name', 'limit');
|
||||
expect(limitInput).toHaveAttribute('data-testid', 'input-Limit-field');
|
||||
expect(limitInput).toHaveAttribute('data-testid', 'input-Limit');
|
||||
});
|
||||
|
||||
it('Cmd+Enter on an input triggers handleRunQuery via container handler', async () => {
|
||||
|
||||
@@ -165,7 +165,7 @@ describe('QueryAddOns', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByTestId('input-Limit-field') as HTMLInputElement;
|
||||
const input = screen.getByTestId('input-Limit') as HTMLInputElement;
|
||||
expect(screen.getByTestId('limit-content')).toBeInTheDocument();
|
||||
expect(input.value).toBe('5');
|
||||
|
||||
@@ -181,8 +181,8 @@ describe('QueryAddOns', () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
[QueryBuilderField.Limit, 'input-Limit-field', 'placeholder'],
|
||||
[QueryBuilderField.Legend, 'input-Legend format-field', 'placeholder'],
|
||||
[QueryBuilderField.Limit, 'input-Limit', 'placeholder'],
|
||||
[QueryBuilderField.Legend, 'input-Legend format', 'placeholder'],
|
||||
[QueryBuilderField.GroupBy, 'groupby', 'data-placeholder'],
|
||||
[QueryBuilderField.Having, 'having-change', 'data-placeholder'],
|
||||
[QueryBuilderField.OrderBy, 'orderby', 'data-placeholder'],
|
||||
@@ -223,7 +223,7 @@ describe('QueryAddOns', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('query-add-on-limit'));
|
||||
|
||||
expect(screen.getByTestId('input-Limit-field')).toHaveAttribute(
|
||||
expect(screen.getByTestId('input-Limit')).toHaveAttribute(
|
||||
'placeholder',
|
||||
'Enter limit',
|
||||
);
|
||||
@@ -247,9 +247,7 @@ describe('QueryAddOns', () => {
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('order-by-content')).toBeInTheDocument();
|
||||
const limitInput = screen.getByTestId(
|
||||
'input-Limit-field',
|
||||
) as HTMLInputElement;
|
||||
const limitInput = screen.getByTestId('input-Limit') as HTMLInputElement;
|
||||
expect(screen.getByTestId('limit-content')).toBeInTheDocument();
|
||||
expect(limitInput.value).toBe('7');
|
||||
});
|
||||
@@ -579,7 +577,7 @@ describe('QueryAddOns', () => {
|
||||
renderHeatmap({ bucketOptions: { kind: 'log', spec: { scale: 0 } } });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Linear' }));
|
||||
await user.type(screen.getByTestId('bucket-options-max-value-field'), '500');
|
||||
await user.type(screen.getByTestId('bucket-options-max-value'), '500');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleChangeQueryData).toHaveBeenLastCalledWith(
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.search {
|
||||
--input-background: var(--l2-background);
|
||||
--input-hover-background: var(--l2-background);
|
||||
--input-focus-background: var(--l2-background);
|
||||
--input-border-color: var(--l2-border);
|
||||
--input-focus-border-color: var(--l2-border);
|
||||
}
|
||||
|
||||
.searchSpinner {
|
||||
color: var(--l2-foreground);
|
||||
animation: spin 0.8s linear infinite;
|
||||
|
||||
@@ -19,9 +19,7 @@ import useActiveQueryIndex from 'components/QuickFilters/hooks/useActiveQueryInd
|
||||
import { DEFAULT_VISIBLE_ITEMS_COUNT } from '../constants';
|
||||
import useCheckboxDisclosure from '../useCheckboxDisclosure';
|
||||
import useCheckboxFilterActions from '../useCheckboxFilterActions';
|
||||
import useCheckboxFilterState, {
|
||||
FILTER_DISABLED_REASON,
|
||||
} from '../useCheckboxFilterState';
|
||||
import useCheckboxFilterState from '../useCheckboxFilterState';
|
||||
import { useFieldValues } from './useFieldValues';
|
||||
import { useExistingQuery } from './useExistingQuery';
|
||||
import { isKeyMatch } from '../utils';
|
||||
@@ -210,8 +208,7 @@ export default function CheckboxFilterV2(
|
||||
placeholder="Filter values"
|
||||
onChange={(e): void => setSearchTextDebounced(e.target.value)}
|
||||
disabled={isFilterDisabled}
|
||||
disabledTooltip={FILTER_DISABLED_REASON}
|
||||
testId="checkbox-filter-search"
|
||||
data-testid="checkbox-filter-search"
|
||||
suffix={
|
||||
isFetching ? (
|
||||
<LoaderCircle
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
align-items: center;
|
||||
justify-items: end;
|
||||
|
||||
// Compact enough to sit inside the 24px value row.
|
||||
--button-height: 21px;
|
||||
--button-padding: var(--spacing-5);
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
await screen.findByTestId('checkbox-value-row-production');
|
||||
expect(screen.getByTestId('checkbox-value-row-staging')).toBeInTheDocument();
|
||||
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search-field');
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search');
|
||||
await user.type(searchInput, 'prod');
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -198,7 +198,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
// Related values now appear in "Related" section (no badge, uses divider instead)
|
||||
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
|
||||
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search-field');
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search');
|
||||
await user.type(searchInput, 'prod');
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -250,7 +250,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
|
||||
await screen.findByTestId('checkbox-value-row-prod');
|
||||
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search-field');
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search');
|
||||
await user.type(searchInput, 'prod');
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -295,7 +295,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
|
||||
await screen.findByTestId('checkbox-value-row-prod');
|
||||
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search-field');
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search');
|
||||
await user.type(searchInput, 'xyz-no-match');
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -404,7 +404,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
|
||||
await screen.findByTestId('checkbox-value-row-pod-a-v1');
|
||||
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search-field');
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search');
|
||||
await user.type(searchInput, 'pod-a');
|
||||
|
||||
// After search, both sections should still appear with filtered results
|
||||
@@ -574,7 +574,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
);
|
||||
|
||||
await screen.findByTestId('checkbox-value-row-value-00');
|
||||
await user.type(screen.getByTestId('checkbox-filter-search-field'), '11');
|
||||
await user.type(screen.getByTestId('checkbox-filter-search'), '11');
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByTestId('checkbox-value-row-value-00'),
|
||||
@@ -586,7 +586,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
await user.click(header);
|
||||
|
||||
await screen.findByTestId('checkbox-value-row-value-00');
|
||||
expect(screen.getByTestId('checkbox-filter-search-field')).toHaveValue('');
|
||||
expect(screen.getByTestId('checkbox-filter-search')).toHaveValue('');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
|
||||
await screen.findByTestId('checkbox-value-row-production');
|
||||
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search-field');
|
||||
const searchInput = screen.getByTestId('checkbox-filter-search');
|
||||
await user.type(searchInput, 'prod');
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Input } from 'antd';
|
||||
import { Slider, type SliderRangeValue } from '@signozhq/ui/slider';
|
||||
import { Slider } from '@signozhq/ui/slider';
|
||||
import { getMs } from 'utils/timeUtils';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
|
||||
@@ -90,13 +90,13 @@ export function DurationSection(props: DurationProps): JSX.Element {
|
||||
debouncedFunction(min, max);
|
||||
};
|
||||
|
||||
const onRangeHandler = (value: SliderRangeValue): void => {
|
||||
const [min, max] = value;
|
||||
const onRangeHandler = (value: number | number[]): void => {
|
||||
const [min, max] = value as number[];
|
||||
updateDurationFilter(min.toString(), max.toString());
|
||||
};
|
||||
|
||||
const formatTooltipValue = useCallback(
|
||||
(value: number): string => `${value.toString()}ms`,
|
||||
const TipComponent = useCallback(
|
||||
(value: number) => <div>{`${value.toString()}ms`}</div>,
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -125,14 +125,13 @@ export function DurationSection(props: DurationProps): JSX.Element {
|
||||
/>
|
||||
</div>
|
||||
<div className="duration-input-slider">
|
||||
<Slider.Range
|
||||
<Slider
|
||||
min={0}
|
||||
max={100000}
|
||||
color="primary"
|
||||
tooltip
|
||||
formatValue={formatTooltipValue}
|
||||
range
|
||||
tooltip={{ formatter: TipComponent }}
|
||||
onChange={(value): void => {
|
||||
const [min, max] = value;
|
||||
const [min, max] = value as number[];
|
||||
onRangeSliderHandler([String(min), String(max)]);
|
||||
}}
|
||||
onAfterChange={onRangeHandler}
|
||||
|
||||
@@ -59,8 +59,7 @@
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
// Query selector themed as the primary sync chip; the Combobox has no
|
||||
// primary color variant.
|
||||
.sync-tag,
|
||||
.select-box {
|
||||
display: flex;
|
||||
padding: 5px 9px;
|
||||
@@ -95,18 +94,6 @@
|
||||
|
||||
.select-box-item {
|
||||
min-width: 60px;
|
||||
text-transform: uppercase;
|
||||
--combobox-trigger-background: var(--primary-background);
|
||||
--combobox-trigger-border: color-mix(
|
||||
in srgb,
|
||||
var(--primary-background) 20%,
|
||||
transparent
|
||||
);
|
||||
--combobox-trigger-border-hover: var(--primary-background);
|
||||
--combobox-trigger-label: var(--primary-foreground);
|
||||
--combobox-trigger-icon: var(--primary-foreground);
|
||||
--combobox-trigger-icon-hover: var(--primary-foreground);
|
||||
--combobox-trigger-min-block-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,14 @@ import {
|
||||
RefreshCw,
|
||||
Settings2 as SettingsIcon,
|
||||
} from '@signozhq/icons';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Combobox, type ComboboxItemType } from '@signozhq/ui/combobox';
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxCommand,
|
||||
ComboboxContent,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
ComboboxTrigger,
|
||||
} from '@signozhq/ui/combobox';
|
||||
import { Skeleton } from 'antd';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
@@ -80,6 +86,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
redirectWithQueryBuilderData,
|
||||
panelType,
|
||||
} = useQueryBuilder();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Sync lastUsedQuery when queries change (e.g., after deletion)
|
||||
const validQueryIndex = useMemo(
|
||||
@@ -106,16 +113,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
}));
|
||||
}, [currentQuery?.builder?.queryData]);
|
||||
|
||||
const queryItems = useMemo<ComboboxItemType[]>(
|
||||
() =>
|
||||
queryOptions.map((option) => ({
|
||||
type: 'item',
|
||||
value: String(option.value),
|
||||
label: option.label,
|
||||
})),
|
||||
[queryOptions],
|
||||
);
|
||||
|
||||
// Show dropdown in ListView only for TRACES_EXPLORER source
|
||||
const shouldShowDropdownInListView =
|
||||
isListView && quickFilterSource === QuickFiltersSource.TRACES_EXPLORER;
|
||||
@@ -191,28 +188,42 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
{queryOptions.length > 1 &&
|
||||
!isAIObservabilityRowView &&
|
||||
(!isListView || shouldShowDropdownInListView) ? (
|
||||
<div className="select-box">
|
||||
<Combobox
|
||||
aria-label="Select a query"
|
||||
<Combobox open={open} onOpenChange={setOpen}>
|
||||
<ComboboxTrigger
|
||||
placeholder="Select a query"
|
||||
searchInputProps={{ placeholder: 'Search queries' }}
|
||||
items={queryItems}
|
||||
value={String(validQueryIndex)}
|
||||
onChange={(value): void => {
|
||||
if (value !== undefined) {
|
||||
handleQueryChange(Number(value));
|
||||
}
|
||||
}}
|
||||
value={queryOptions.find((f) => f.value === validQueryIndex)?.label || ''}
|
||||
className="select-box"
|
||||
/>
|
||||
</div>
|
||||
{open && (
|
||||
<ComboboxContent>
|
||||
<ComboboxCommand>
|
||||
<ComboboxList>
|
||||
{queryOptions.map((option) => (
|
||||
<ComboboxItem
|
||||
key={option.value}
|
||||
value={String(option.value)}
|
||||
onSelect={(): void => {
|
||||
handleQueryChange(option.value);
|
||||
setOpen(false);
|
||||
}}
|
||||
isSelected={validQueryIndex === option.value}
|
||||
>
|
||||
{option.label}
|
||||
</ComboboxItem>
|
||||
))}
|
||||
</ComboboxList>
|
||||
</ComboboxCommand>
|
||||
</ComboboxContent>
|
||||
)}
|
||||
</Combobox>
|
||||
) : (
|
||||
displayedQueryName && (
|
||||
<Tooltip
|
||||
title={`Filter currently in sync with query ${displayedQueryName}`}
|
||||
>
|
||||
<Badge variant="solid" color="primary" textTransform="uppercase">
|
||||
<Typography.Text className="sync-tag">
|
||||
{displayedQueryName}
|
||||
</Badge>
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -98,15 +98,6 @@ export const noSavedFiltersHandler = rest.get(
|
||||
),
|
||||
);
|
||||
|
||||
export const noSavedTracesFiltersHandler = rest.get(
|
||||
'http://localhost/api/v2/quick_filters/traces',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json(quickFiltersResponse(QuickfiltertypesSourceDTO.traces, [])),
|
||||
),
|
||||
);
|
||||
|
||||
export const loadingFiltersHandlers = [
|
||||
rest.get('http://localhost/api/v2/quick_filters/logs', (_req, res, ctx) =>
|
||||
res(ctx.delay('infinite')),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import type { ComponentProps, ComponentType } from 'react';
|
||||
import { expect, screen, userEvent, waitFor, within } from 'storybook/test';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { withCanvas } from '@/storybook/decorators/withCanvas';
|
||||
import type { GlobalMockArgs } from '@/storybook/globals';
|
||||
@@ -17,9 +16,8 @@ import {
|
||||
queryBuilder,
|
||||
selectedServiceQueryBuilder,
|
||||
noSavedFiltersHandler,
|
||||
noSavedTracesFiltersHandler,
|
||||
} from './QuickFilters.stories.mocks';
|
||||
import { FiltersType, QuickFiltersSource } from '../types';
|
||||
import { QuickFiltersSource } from '../types';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Quick Filters',
|
||||
@@ -60,10 +58,10 @@ type TooltipsStory = StoryObj<
|
||||
* silence.
|
||||
*/
|
||||
const settingsControl = (canvasElement: HTMLElement): Promise<HTMLElement> =>
|
||||
waitFor(async () => {
|
||||
waitFor(() => {
|
||||
const control = within(canvasElement).getByTestId('settings-icon-container');
|
||||
|
||||
await expect(control).not.toHaveAttribute('aria-disabled', 'true');
|
||||
expect(control).not.toHaveAttribute('aria-disabled', 'true');
|
||||
|
||||
return control;
|
||||
});
|
||||
@@ -112,29 +110,6 @@ export const SelectedExpandedCheckbox: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
/** The duration min/max row, which a checkbox-only config never reaches. */
|
||||
export const DurationFilter: Story = {
|
||||
args: {
|
||||
quickFilterSource: QuickFiltersSource.TRACES_EXPLORER,
|
||||
config: [
|
||||
...checkboxConfig,
|
||||
{
|
||||
attributeKey: {
|
||||
dataType: DataTypes.Float64,
|
||||
key: 'duration_nano',
|
||||
type: '',
|
||||
},
|
||||
defaultOpen: true,
|
||||
title: 'Duration',
|
||||
type: FiltersType.DURATION,
|
||||
},
|
||||
],
|
||||
},
|
||||
parameters: {
|
||||
msw: { handlers: [noSavedTracesFiltersHandler, ...handlers] },
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Every tooltip the panel renders, held open: the reveal on each truncated
|
||||
* filter value. Nothing bounds those values, so the panel is answered with
|
||||
|
||||
@@ -182,29 +182,15 @@ describe('Quick Filters', () => {
|
||||
expect(screen.getByRole('option', { name: 'Query C' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Select Query B with the keyboard: jsdom cannot deliver the pointer
|
||||
// click Base UI expects, so filter through the search row and pick with
|
||||
// Enter instead
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('combobox', { name: 'Search queries' }),
|
||||
).toHaveFocus();
|
||||
});
|
||||
await user.keyboard('Query B');
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByRole('option', { name: 'Query A' }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
await user.keyboard('{ArrowDown}{Enter}');
|
||||
// Select Query B
|
||||
const queryBOption = screen.getByRole('option', { name: 'Query B' });
|
||||
await user.click(queryBOption);
|
||||
|
||||
// Verify setLastUsedQuery was called with index 1
|
||||
await waitFor(() => {
|
||||
expect(setLastUsedQuery).toHaveBeenCalledWith(1);
|
||||
});
|
||||
// filtering and picking through the search row takes Base UI past the
|
||||
// default 5s under jsdom
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it('should not display query dropdown in ListView', () => {
|
||||
(useQueryBuilder as jest.Mock).mockReturnValue({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Check, Copy, SolidInfoCircle } from '@signozhq/icons';
|
||||
import { Check, Copy } from '@signozhq/icons';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
@@ -46,9 +46,11 @@ function KeyCreatedPhase({
|
||||
</div>
|
||||
|
||||
<div className="add-key-modal__callout-wrapper">
|
||||
<Callout color="info" size="sm" icon={<SolidInfoCircle />}>
|
||||
Store the key securely. This is the only time it will be displayed.
|
||||
</Callout>
|
||||
<Callout
|
||||
type="info"
|
||||
showIcon
|
||||
title="Store the key securely. This is the only time it will be displayed."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user