mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-20 17:11:08 +01:00
Compare commits
9 Commits
docs/authz
...
chore/open
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a8cfddc9f | ||
|
|
5f3896a096 | ||
|
|
2cd7285e3e | ||
|
|
63924a0fbb | ||
|
|
9127c5abf7 | ||
|
|
806a51269c | ||
|
|
37f3cd4403 | ||
|
|
65fc5a9cbb | ||
|
|
03bdeb9ba4 |
43
.github/workflows/e2eci.yaml
vendored
43
.github/workflows/e2eci.yaml
vendored
@@ -2,25 +2,13 @@ name: e2eci
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- labeled
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
merge_group:
|
||||
types:
|
||||
- checks_requested
|
||||
|
||||
concurrency:
|
||||
group: e2eci-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
if: github.event_name != 'merge_group'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
e2e: ${{ steps.changes.outputs.e2e }}
|
||||
@@ -30,17 +18,21 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
LABELED: ${{ contains(github.event.pull_request.labels.*.name, 'safe-to-e2e') }}
|
||||
FORCE: ${{ contains(github.event.pull_request.labels.*.name, 'safe-to-e2e') }}
|
||||
run: |
|
||||
files=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename')
|
||||
if [ "$LABELED" = true ] || grep -qE '^(frontend|tests/e2e)/' <<< "$files"; then
|
||||
CHANGED_FILES=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename | select(startswith("tests/integration/") | not)')
|
||||
PATTERN='^(frontend/|tests/|\.github/workflows/e2eci\.yaml$|cmd/enterprise/Dockerfile\.with-web\.integration$)'
|
||||
|
||||
if [ "$FORCE" = "true" ]; then
|
||||
echo "e2e=true" >> "$GITHUB_OUTPUT"
|
||||
elif printf '%s\n' "$CHANGED_FILES" | grep -qE "$PATTERN"; then
|
||||
echo "e2e=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "e2e=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
fmtlint:
|
||||
needs: changes
|
||||
if: |
|
||||
((github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))) && needs.changes.outputs.e2e == 'true'
|
||||
if: needs.changes.outputs.e2e == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
@@ -70,9 +62,7 @@ jobs:
|
||||
- chromium
|
||||
shard: [1, 2, 3, 4]
|
||||
needs: changes
|
||||
if: |
|
||||
((github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))) && needs.changes.outputs.e2e == 'true'
|
||||
if: needs.changes.outputs.e2e == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
@@ -181,17 +171,14 @@ jobs:
|
||||
name: playwright-artifacts-${{ matrix.project }}-${{ matrix.shard }}
|
||||
path: tests/e2e/artifacts/
|
||||
retention-days: 5
|
||||
# Required status check. Fails when e2e was needed but did not pass,
|
||||
# including fork and dependabot PRs still waiting on safe-to-test.
|
||||
# Required status check. Fails when e2e was needed but did not pass.
|
||||
e2e:
|
||||
if: always()
|
||||
needs: [changes, fmtlint, test]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: verdict
|
||||
if: |
|
||||
github.event_name != 'merge_group' && (needs.changes.result != 'success' ||
|
||||
(needs.changes.outputs.e2e == 'true' && (needs.fmtlint.result != 'success' || needs.test.result != 'success')))
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
|
||||
run: |
|
||||
echo "::error::e2e did not pass. Fork and dependabot PRs need the safe-to-test label."
|
||||
echo "::error::e2e did not pass."
|
||||
exit 1
|
||||
|
||||
@@ -251,6 +251,7 @@ components:
|
||||
opsgenie: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelOpsgenieConfig'
|
||||
pagerduty: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelPagerdutyConfig'
|
||||
slack: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelSlackConfig'
|
||||
telegram: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelTelegramConfig'
|
||||
webhook: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelWebhookConfig'
|
||||
propertyName: kind
|
||||
oneOf:
|
||||
@@ -264,6 +265,7 @@ components:
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJiraConfig'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJSMOpsConfig'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelIncidentIOConfig'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelTelegramConfig'
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelEmailConfig:
|
||||
properties:
|
||||
@@ -373,6 +375,18 @@ components:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelTelegramConfig:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- telegram
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelTelegramConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelWebhookConfig:
|
||||
properties:
|
||||
kind:
|
||||
@@ -525,6 +539,7 @@ components:
|
||||
- jira
|
||||
- jsmops
|
||||
- incidentio
|
||||
- telegram
|
||||
type: string
|
||||
AlertmanagertypesChannelListOrder:
|
||||
enum:
|
||||
@@ -729,6 +744,31 @@ components:
|
||||
- title
|
||||
- value
|
||||
type: object
|
||||
AlertmanagertypesChannelTelegramConfig:
|
||||
properties:
|
||||
apiUrl:
|
||||
type: string
|
||||
botToken:
|
||||
format: password
|
||||
type: string
|
||||
chatId:
|
||||
format: int64
|
||||
type: integer
|
||||
disableNotifications:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
messageThreadId:
|
||||
type: integer
|
||||
sendResolved:
|
||||
nullable: true
|
||||
type: boolean
|
||||
title:
|
||||
type: string
|
||||
required:
|
||||
- botToken
|
||||
- chatId
|
||||
type: object
|
||||
AlertmanagertypesChannelWebhookConfig:
|
||||
properties:
|
||||
bearerToken:
|
||||
@@ -1022,6 +1062,8 @@ components:
|
||||
- jsmops_configs
|
||||
- required:
|
||||
- incidentio_configs
|
||||
- required:
|
||||
- telegram_configs
|
||||
- required:
|
||||
- discord_configs
|
||||
- required:
|
||||
@@ -1042,8 +1084,6 @@ components:
|
||||
- victorops_configs
|
||||
- required:
|
||||
- sns_configs
|
||||
- required:
|
||||
- telegram_configs
|
||||
- required:
|
||||
- webex_configs
|
||||
- required:
|
||||
@@ -1119,7 +1159,7 @@ components:
|
||||
type: array
|
||||
telegram_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigTelegramConfig'
|
||||
$ref: '#/components/schemas/AlertmanagertypesTelegramReceiverConfig'
|
||||
type: array
|
||||
victorops_configs:
|
||||
items:
|
||||
@@ -1263,7 +1303,7 @@ components:
|
||||
type: array
|
||||
telegram_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigTelegramConfig'
|
||||
$ref: '#/components/schemas/AlertmanagertypesTelegramReceiverConfig'
|
||||
type: array
|
||||
victorops_configs:
|
||||
items:
|
||||
@@ -1334,6 +1374,34 @@ components:
|
||||
- timezone
|
||||
- startTime
|
||||
type: object
|
||||
AlertmanagertypesTelegramReceiverConfig:
|
||||
properties:
|
||||
api_url:
|
||||
$ref: '#/components/schemas/ConfigURLType2'
|
||||
chat:
|
||||
format: int64
|
||||
type: integer
|
||||
chat_file:
|
||||
type: string
|
||||
disable_notifications:
|
||||
type: boolean
|
||||
http_config:
|
||||
$ref: '#/components/schemas/ConfigHTTPClientConfig'
|
||||
message:
|
||||
type: string
|
||||
message_thread_id:
|
||||
type: integer
|
||||
parse_mode:
|
||||
type: string
|
||||
send_resolved:
|
||||
type: boolean
|
||||
title:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
token_file:
|
||||
type: string
|
||||
type: object
|
||||
AlertmanagertypesTestableNotificationChannel:
|
||||
properties:
|
||||
config:
|
||||
@@ -1374,9 +1442,13 @@ components:
|
||||
AuthtypesAuthDomainConfigGoogle:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
description: The kind of authn provider.
|
||||
spec:
|
||||
$ref: '#/components/schemas/AuthtypesGoogleConfig'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthtypesGoogleConfig'
|
||||
description: The google auth configuration.
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
@@ -1384,9 +1456,13 @@ components:
|
||||
AuthtypesAuthDomainConfigOIDC:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
description: The kind of authn provider.
|
||||
spec:
|
||||
$ref: '#/components/schemas/AuthtypesOIDCConfig'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthtypesOIDCConfig'
|
||||
description: The oidc configuration.
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
@@ -1394,9 +1470,13 @@ components:
|
||||
AuthtypesAuthDomainConfigSAML:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
description: The kind of authn provider.
|
||||
spec:
|
||||
$ref: '#/components/schemas/AuthtypesSamlConfig'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthtypesSamlConfig'
|
||||
description: The saml configuration.
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
@@ -8152,9 +8232,13 @@ components:
|
||||
Querybuildertypesv5BucketOptionsLinear:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5BucketsKind'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5BucketsKind'
|
||||
description: How the upper bounds are spaced.
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5LinearBucketsSpec'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5LinearBucketsSpec'
|
||||
description: The evenly spaced bucket specification.
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
@@ -8162,9 +8246,13 @@ components:
|
||||
Querybuildertypesv5BucketOptionsLog:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5BucketsKind'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5BucketsKind'
|
||||
description: How the upper bounds are spaced.
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5LogBucketsSpec'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5LogBucketsSpec'
|
||||
description: The logarithmic bucket specification.
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
@@ -8753,54 +8841,78 @@ components:
|
||||
Querybuildertypesv5QueryEnvelopeBuilder:
|
||||
properties:
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5BuilderQuerySpec'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5BuilderQuerySpec'
|
||||
description: The builder query specification.
|
||||
type:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
description: The type of the query.
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
Querybuildertypesv5QueryEnvelopeBuilderAI:
|
||||
properties:
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation'
|
||||
description: The AI builder query specification.
|
||||
type:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
description: The type of the query.
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
Querybuildertypesv5QueryEnvelopeClickHouseSQL:
|
||||
properties:
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5ClickHouseQuery'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5ClickHouseQuery'
|
||||
description: The ClickHouse SQL query specification.
|
||||
type:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
description: The type of the query.
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
Querybuildertypesv5QueryEnvelopeFormula:
|
||||
properties:
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderFormula'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderFormula'
|
||||
description: The formula specification.
|
||||
type:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
description: The type of the query.
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
Querybuildertypesv5QueryEnvelopePromQL:
|
||||
properties:
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5PromQuery'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5PromQuery'
|
||||
description: The PromQL query specification.
|
||||
type:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
description: The type of the query.
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
Querybuildertypesv5QueryEnvelopeTraceOperator:
|
||||
properties:
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderTraceOperator'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderTraceOperator'
|
||||
description: The trace operator specification.
|
||||
type:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
description: The type of the query.
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
@@ -9360,9 +9472,13 @@ components:
|
||||
RuletypesEvaluationCumulative:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/RuletypesEvaluationKind'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesEvaluationKind'
|
||||
description: The kind of evaluation.
|
||||
spec:
|
||||
$ref: '#/components/schemas/RuletypesCumulativeWindow'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesCumulativeWindow'
|
||||
description: The cumulative window evaluation specification.
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
@@ -9385,9 +9501,13 @@ components:
|
||||
RuletypesEvaluationRolling:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/RuletypesEvaluationKind'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesEvaluationKind'
|
||||
description: The kind of evaluation.
|
||||
spec:
|
||||
$ref: '#/components/schemas/RuletypesRollingWindow'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesRollingWindow'
|
||||
description: The rolling window evaluation specification.
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
@@ -9768,9 +9888,13 @@ components:
|
||||
RuletypesThresholdBasic:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/RuletypesThresholdKind'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesThresholdKind'
|
||||
description: The kind of threshold.
|
||||
spec:
|
||||
$ref: '#/components/schemas/RuletypesBasicRuleThresholds'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuletypesBasicRuleThresholds'
|
||||
description: The basic threshold specification (array of thresholds).
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
|
||||
@@ -277,10 +277,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
lb.Set(ruletypes.AlertRuleIDLabel, r.ID())
|
||||
lb.Set(ruletypes.RuleSourceLabel, r.GeneratorURL())
|
||||
|
||||
annotations := make(ruletypes.Labels, 0, len(r.Annotations().Map()))
|
||||
for name, value := range r.Annotations().Map() {
|
||||
annotations = append(annotations, ruletypes.Label{Name: name, Value: expand(value)})
|
||||
}
|
||||
annotations := r.ExpandAnnotations(expand)
|
||||
if smpl.IsMissing {
|
||||
lb.Set(ruletypes.AlertNameLabel, "[No data] "+r.Name())
|
||||
lb.Set(ruletypes.NoDataLabel, "true")
|
||||
|
||||
@@ -262,23 +262,20 @@
|
||||
// React Hooks rules - Enforces Rules of Hooks (only call at top level)
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
// Warns about missing dependencies in useEffect/useMemo/useCallback
|
||||
// NOTE: The following react-hooks rules are not supported right know, follow the progress at https://github.com/oxc-project/oxc/issues/1022
|
||||
// Most of them are for React Compiler, which we don't have enabled right know
|
||||
// "react-hooks/config": "error",
|
||||
// "react-hooks/error-boundaries": "error",
|
||||
// "react-hooks/component-hook-factories": "error",
|
||||
// "react-hooks/gating": "error",
|
||||
// "react-hooks/globals": "error",
|
||||
// "react-hooks/immutability": "error",
|
||||
// "react-hooks/preserve-manual-memoization": "error",
|
||||
// "react-hooks/purity": "error",
|
||||
// "react-hooks/refs": "error",
|
||||
// "react-hooks/set-state-in-effect": "error",
|
||||
// "react-hooks/set-state-in-render": "error",
|
||||
// "react-hooks/static-components": "error",
|
||||
// "react-hooks/unsupported-syntax": "warn",
|
||||
// "react-hooks/use-memo": "error",
|
||||
// "react-hooks/incompatible-library": "warn",
|
||||
// React Compiler rules, off because we don't have React Compiler enabled
|
||||
"react/error-boundaries": "off",
|
||||
"react/globals": "off",
|
||||
"react/immutability": "off",
|
||||
"react/incompatible-library": "off",
|
||||
"react/preserve-manual-memoization": "off",
|
||||
"react/purity": "off",
|
||||
"react/refs": "off",
|
||||
"react/set-state-in-effect": "off",
|
||||
"react/set-state-in-render": "off",
|
||||
"react/static-components": "off",
|
||||
"react/unsupported-syntax": "off",
|
||||
"react/use-memo": "off",
|
||||
"react/void-use-memo": "off",
|
||||
"signoz/no-unsupported-asset-pattern": "error",
|
||||
// Prevents the wrong usage of assets to break custom base path installations
|
||||
"signoz/no-zustand-getstate-in-hooks": "error",
|
||||
@@ -582,6 +579,16 @@
|
||||
"rules": {
|
||||
"signoz/no-dashboard-fetch-outside-root": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
// TypeScript reports real redeclarations; this rule also flags `const X` + `type X` pairs.
|
||||
"files": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"no-redeclare": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -204,9 +204,9 @@
|
||||
"lint-staged": "^17.0.4",
|
||||
"msw": "1.3.2",
|
||||
"orval": "8.22.0",
|
||||
"oxfmt": "0.54.0",
|
||||
"oxlint": "1.69.0",
|
||||
"oxlint-tsgolint": "0.23.0",
|
||||
"oxfmt": "0.73.0",
|
||||
"oxlint": "1.88.0",
|
||||
"oxlint-tsgolint": "7.0.2004",
|
||||
"postcss": "8.5.26",
|
||||
"postcss-scss": "4.0.9",
|
||||
"react-resizable": "3.0.4",
|
||||
|
||||
403
frontend/pnpm-lock.yaml
generated
403
frontend/pnpm-lock.yaml
generated
@@ -487,14 +487,14 @@ importers:
|
||||
specifier: 8.22.0
|
||||
version: 8.22.0(prettier@3.8.3)(typescript@5.9.3)
|
||||
oxfmt:
|
||||
specifier: 0.54.0
|
||||
version: 0.54.0
|
||||
specifier: 0.73.0
|
||||
version: 0.73.0
|
||||
oxlint:
|
||||
specifier: 1.69.0
|
||||
version: 1.69.0(oxlint-tsgolint@0.23.0)
|
||||
specifier: 1.88.0
|
||||
version: 1.88.0(oxlint-tsgolint@7.0.2004)
|
||||
oxlint-tsgolint:
|
||||
specifier: 0.23.0
|
||||
version: 0.23.0
|
||||
specifier: 7.0.2004
|
||||
version: 7.0.2004
|
||||
postcss:
|
||||
specifier: 8.5.26
|
||||
version: 8.5.26
|
||||
@@ -533,7 +533,7 @@ importers:
|
||||
version: 1.6.0(react@18.2.0)
|
||||
vite-plugin-checker:
|
||||
specifier: 0.12.0
|
||||
version: 0.12.0(eslint@10.2.1(jiti@2.6.1))(meow@13.2.0)(optionator@0.9.4)(oxlint@1.69.0(oxlint-tsgolint@0.23.0))(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(stylelint@17.15.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
version: 0.12.0(eslint@10.2.1(jiti@2.6.1))(meow@13.2.0)(optionator@0.9.4)(oxlint@1.88.0(oxlint-tsgolint@7.0.2004))(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(stylelint@17.15.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
vite-plugin-compression:
|
||||
specifier: 0.5.1
|
||||
version: 0.5.1(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))
|
||||
@@ -2555,276 +2555,276 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxfmt/binding-android-arm-eabi@0.54.0':
|
||||
resolution: {integrity: sha512-NAtpl/SiaeU103e7/OmZw0MvUnsUUopW7hEm/ecegJg7YM0skQaA0IXEZoyTV6NUdiNPupdIUreRqUZTShbn/g==}
|
||||
'@oxfmt/binding-android-arm-eabi@0.73.0':
|
||||
resolution: {integrity: sha512-dhunAdMbi/4o0Y66/+Wl6rpsNqpvE4i26Qw6bB77A97L2lJoktWg6m+X9QOon8iOEEqzD9rEZqFi5uZ4oIzhCA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@oxfmt/binding-android-arm64@0.54.0':
|
||||
resolution: {integrity: sha512-B4VZfBUlKK1rmMChsssNZbkZjE8+FzG3avMjGgMDwbGxXRoXkoeXiAZ+78Oa+eyDPHvDCiUb4zH/vmCOUSafLQ==}
|
||||
'@oxfmt/binding-android-arm64@0.73.0':
|
||||
resolution: {integrity: sha512-dBoypjC2/xeyKyIhN2waX0/XeoccH9k0lVVe+sGRc4X8HSJVRtqyZDoXdr7bGY9A75c51dX0z0dLDpGQKwCdCg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@oxfmt/binding-darwin-arm64@0.54.0':
|
||||
resolution: {integrity: sha512-i02vF75b+ePsQP3tHqSxVYI5S6b8X/xqdPu7/mDHXtpgXLTYXi3jJmfHU0j+dnZZDKaYTx/ioCK7QYJmtiJR2g==}
|
||||
'@oxfmt/binding-darwin-arm64@0.73.0':
|
||||
resolution: {integrity: sha512-Tn7Y+qWIpCQK+qdYwBcn6YvqeBuPiuG3tcNlYg16a5QL2o3rSmrgBB1nCyYqo0uvc1LNrfQW9/CK2tzFuID/Fg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxfmt/binding-darwin-x64@0.54.0':
|
||||
resolution: {integrity: sha512-8VMFvGvooXj7mswkbrhdVZ2/sgiDaBzWpkkbtO+qGDLV4EfJd67nQadHkQC0ZNbaWA9ajXfqI6i7PZLIeDzxEQ==}
|
||||
'@oxfmt/binding-darwin-x64@0.73.0':
|
||||
resolution: {integrity: sha512-yVb0E0tg/+9Xc9YJCBq66qF1DC/aN5mhb5nnHz0k4ikwkMANtSul2VPDSwjYdXR6/DFimc+pPUgLBqkIa5Ctuw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxfmt/binding-freebsd-x64@0.54.0':
|
||||
resolution: {integrity: sha512-0cRHnp43WN1Jrc5s0BdbdKgR1XirdvHy7TAFi3JEsoEVQVJxTXMbpVd76sxXlgRswNMDhVFSJw+y7Eb8mEavFQ==}
|
||||
'@oxfmt/binding-freebsd-x64@0.73.0':
|
||||
resolution: {integrity: sha512-IKR4lFnBkYNL9Uqdxa5GMTQLvnLj+3sDwTvh2atHCzah0xeo+1xR5bigRU8uyXlOsA4VpXUn0UAimq4E4RBGmQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@oxfmt/binding-linux-arm-gnueabihf@0.54.0':
|
||||
resolution: {integrity: sha512-JyQAk3hK/OEtup7Rw6kZwfdzbKqTVD5jXXb8Xpfay29suwZyfBDMVW/bj4RqEPySYWc6zCp198pOluf8n5uYzg==}
|
||||
'@oxfmt/binding-linux-arm-gnueabihf@0.73.0':
|
||||
resolution: {integrity: sha512-7qnZ4g0ZCO3aZevOOQpZIABYwwXJW1lLDo2elviGdPQ0i11255xKEMFsi97fC1/nauQFseCJfELUy/k2JoOP+g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxfmt/binding-linux-arm-musleabihf@0.54.0':
|
||||
resolution: {integrity: sha512-qnvLatTpM8vtvjOfcckBOzJjk+n6ce/wwpP8OFeUrD5aNLYcKyWAitwj+Rk3PK9jGanbZvKsJnv14JGQ6XqFdw==}
|
||||
'@oxfmt/binding-linux-arm-musleabihf@0.73.0':
|
||||
resolution: {integrity: sha512-36eBico2j/WPB84gb4FYd/jjqGCKOwU4M+TWLBUIqlnzTozQueLy0joA5BwsSp2OZFVkOlpCwvA3ndPamdiyLw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxfmt/binding-linux-arm64-gnu@0.54.0':
|
||||
resolution: {integrity: sha512-SMkhnCzIYZYDk9vw3W/80eeYKmrMpGF0Giuxt4HruFlCH7jEtnPeb3SdQKMfgYi/dgtaf+hZAb5XWPYnxqCQ3w==}
|
||||
'@oxfmt/binding-linux-arm64-gnu@0.73.0':
|
||||
resolution: {integrity: sha512-VZ3cSgCAT18knKo52DcACmAuIoqG6qVEyeZRFbJd0xmYQ8GyYjuBc6IAh6MZHijT6q7p4/HPI+6ZWsCuhdV6iw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-arm64-musl@0.54.0':
|
||||
resolution: {integrity: sha512-QrwJlBFFKnxOd95TAaszpMbZBLzMoYMpGaQTZF8oibacnF5rv8l12IhILhQRPmksWiBqg0YSe2Mnl7ayeJAHSA==}
|
||||
'@oxfmt/binding-linux-arm64-musl@0.73.0':
|
||||
resolution: {integrity: sha512-8YVzH59XSHcqwXNgc5qK851zCqwpmBgOifARXEPPe5/xTsIbHceYoPZAyqfpYV0CfKmzSaEDBgYHFza4rhx+Zw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.54.0':
|
||||
resolution: {integrity: sha512-WILatiol/TUHTlhod7R09+7Az/XlhKwmY1MHfLZNmewltPWNN/EwxP2rQSHahibZ/cB8gmckEBjBOByD+5bYsQ==}
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.73.0':
|
||||
resolution: {integrity: sha512-MqRJnmCys0rwOgpWyEidSA7v/bvOqfNDJhPkQoR9mPVYrvSmMaW2Xhdpl28cbTA+brOM1is62GjIvJplVlUVpw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.54.0':
|
||||
resolution: {integrity: sha512-f05YMG4BH4G8S4ME6UM6fi1MnJ9094mrnvO5Pa4SJlMfWlUM+1/ZWMEF4NnjM7shZAvbHsHRuVYpUo0PHC4P9Q==}
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.73.0':
|
||||
resolution: {integrity: sha512-4t2F3SWJSK/O/kRRFnL0elJzn75fJGGu71gs1HtiWcS80SmJOpkHiwwLnTp9suGdns8VHEPxYS0GTcyVwHKWBA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.54.0':
|
||||
resolution: {integrity: sha512-UfL+2hj1ClNqcCRT9s8vBU4axDpjxgVxX96G+9DYAYjoc5b0u15CJtn2jgsi9iM+EbGNc5CW1HVRgwVu76UsSA==}
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.73.0':
|
||||
resolution: {integrity: sha512-D8b4I8I1kLdJQFwdFXNV+cwh3xWSiNKofjdGUk0nDpc0AHOds8T+poGbP9RWQMvgsvMb9OMgp/aV7ahBR37TbQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.54.0':
|
||||
resolution: {integrity: sha512-3/XZe931Hka+J6NjnaqJzYpsWWxDTuRdUdwSQHnOuJEgbC+SehIMFJS8hsEjV7LBhVSL2OCnRLvbVW8O97XIyw==}
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.73.0':
|
||||
resolution: {integrity: sha512-QrC1S5VL1+N5rvorB15jH7CPvjVHA7GyUI5oVEd8Bd9m5605RDBQYTxDQ920DuylRoZOh2V7/xXpfCtFUrojUQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-gnu@0.54.0':
|
||||
resolution: {integrity: sha512-Ik93RlObtu43GbxApafayFjwYE06L6Xr08cSwpBPYbDrLp2ReZx0Jm1DqwRyYRnukUJy+rK2WaEvUQOxdytU9Q==}
|
||||
'@oxfmt/binding-linux-x64-gnu@0.73.0':
|
||||
resolution: {integrity: sha512-dFcUKQPltCEPa+YlkF3zqufn6nJPZdG73LpnjBvZFFsU3laCMssDXSYB99mVaj43FqdEgjpz5TO6gbYXcczd9g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-musl@0.54.0':
|
||||
resolution: {integrity: sha512-yZcakmPlD86CNymknd7KfW+FH+qfbqJH+i0h69CYfV1+KMoVeM9UED+8+TDVoU4haxI0NxY7RPCvRLy3Sqd2Qg==}
|
||||
'@oxfmt/binding-linux-x64-musl@0.73.0':
|
||||
resolution: {integrity: sha512-LmQe+nV8e+eUwfPNCxDF+owbsg88toBiR/8rZRgVOeKQP3MaGefXZRvBoc1UG37iWEQ/9JlCXG9Mc4nf4N3agw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-openharmony-arm64@0.54.0':
|
||||
resolution: {integrity: sha512-GiVBZNnEZnKu00f1jTg49nomv187d0GQX+O+ocykoLeiaALuEO+swoTehHn9TehTfi7V8H0i0e/yvUjCqnwk1w==}
|
||||
'@oxfmt/binding-openharmony-arm64@0.73.0':
|
||||
resolution: {integrity: sha512-8weOr1OKknE0G2DbR6ejJUT9AOsoBU6cXlsetmbihHv1K+bK7qP61T6lJxY0k/aAO7GyOJiRGNlL+04r4eH8Xg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@oxfmt/binding-win32-arm64-msvc@0.54.0':
|
||||
resolution: {integrity: sha512-J0SSB8Z1Fre2sxRolYcW6Rl1RQmKdQ2hnHyq4YJrfBRiXTObLw4DXnIVraM/UyqGqwOi7yTrQA4VT7DPxlHVKA==}
|
||||
'@oxfmt/binding-win32-arm64-msvc@0.73.0':
|
||||
resolution: {integrity: sha512-mZV+QxXYgb8E18gzAFItb937VIsE4dqWdStrD3tRRC52UubOYEuU+e7NaIqUsGdB5YsJqNeuck3Ue/af2r9wyA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxfmt/binding-win32-ia32-msvc@0.54.0':
|
||||
resolution: {integrity: sha512-O61UDVj8zz6yXJjkHPf05VaMLOXmEF8P5kf/N0W7AQMmd6bcQogl+KJc7rMutKTL524oE9iH32JXZClBFmEQIg==}
|
||||
'@oxfmt/binding-win32-ia32-msvc@0.73.0':
|
||||
resolution: {integrity: sha512-QJPUsbgUb97ZPPMkFUq6VQq0fLhC4Np19KL9aB/qKVwXnpKE6MIiILXqzhKnQ/UbnEDjKJ7Xv1d6zH4bYPCUdQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@oxfmt/binding-win32-x64-msvc@0.54.0':
|
||||
resolution: {integrity: sha512-1MDpqJPiFqxWtIHas8vkb1VZ7f7eKyTffAwmO8isxQYMaG1OFKsH666BWLeXQLO+IWNfiMssLD55hbR1lIPTqg==}
|
||||
'@oxfmt/binding-win32-x64-msvc@0.73.0':
|
||||
resolution: {integrity: sha512-cGcze2fu+ykIaFtkEvZlkZ3HvnCLk+WISthtq99zWTWn0x13t5L9TxvGdLZMUMvtfYR+ICkiCwKnguVubdcw0w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@0.23.0':
|
||||
resolution: {integrity: sha512-gOs9PVr2wEg4ox9z0aJo+RKhhImW86YL5N6yav8BK/rgPsIrwN/igSZ+pbRr723NFvUNKde9fgMhRA6JrXAOZw==}
|
||||
'@oxlint-tsgolint/darwin-arm64@7.0.2004':
|
||||
resolution: {integrity: sha512-uJk3ttju7Lz+Nv8WG4CsLyewoL1qf/Ny8S8OksJ7Bm/F14Tc9gMfW6arM1PCc57IzH/X/I79YsCWTgxX1FKibQ==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint-tsgolint/darwin-x64@0.23.0':
|
||||
resolution: {integrity: sha512-kjJ8B+7n4tB9VJdxS5A9GdJt6/bYpzbu4lXp2uO1S3sRmCB5gDEABlGoiePNApRWaW+xqL4b4xgiE727jSLhuA==}
|
||||
'@oxlint-tsgolint/darwin-x64@7.0.2004':
|
||||
resolution: {integrity: sha512-0BFfHTzDxNDFPcu0UZAXT6CyCJHAXM0dZveyDb4KEHa2qF/DSnOKOrmvK/wFuFol9kvijbGM9JfuOsor+zWXRw==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint-tsgolint/linux-arm64@0.23.0':
|
||||
resolution: {integrity: sha512-6dCZuKNu135seMXilkRk9SpCx6i1XgmiipYGalLij5WVRX6ZYS8c4xI7preN/zv9fCXhsQclTIMDu2Y/cytTjw==}
|
||||
'@oxlint-tsgolint/linux-arm64@7.0.2004':
|
||||
resolution: {integrity: sha512-TAehQc+qdRXyDN3nl/r/3/oPrBVrbtrDeGMkFyG6mf+XphpoJ2lAMVFLclyvrVZCw02RbKROjyw8h72XwQQHcw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint-tsgolint/linux-x64@0.23.0':
|
||||
resolution: {integrity: sha512-3bdilnyA7kmSTjK27rvjIjSxL5SIg3wt7vwNiRkouWB83ytssyKnuGvxSYJxgMEmFpSutzaBzcCUM2jDtPGcgA==}
|
||||
'@oxlint-tsgolint/linux-x64@7.0.2004':
|
||||
resolution: {integrity: sha512-LPunZvm6ZbWPfHMAyfZf9ci3HHvNEzLejO8zdqO3GoVGoSEHqqufHUD7+OgeatVSaJP7uXDJ2HaNOHIoH7go4Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint-tsgolint/win32-arm64@0.23.0':
|
||||
resolution: {integrity: sha512-j+OEp44SVYiQ+ZD+uttsX7u6L9SvmbbQ77SO1pSFCcJlsVMeCk8qZsjhKfGKuT/jIA+ipOJMVs/+pqUfObBWNw==}
|
||||
'@oxlint-tsgolint/win32-arm64@7.0.2004':
|
||||
resolution: {integrity: sha512-FVWplzDImWhiuawnIZVETaQecv4GWom6Jv4Fx2xj7hhXXDZ5okEgiw1gTCImHTIQsi2ycC5oSxVwrn69bMh7iw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint-tsgolint/win32-x64@0.23.0':
|
||||
resolution: {integrity: sha512-5MyjFuqf+g8OUPJBSGWHJtmoWnzFJYyOg4To9WMQshZYEWig/vtu7JtJ03VWnzHv9LJkAUeApY0gVCOywFR/iQ==}
|
||||
'@oxlint-tsgolint/win32-x64@7.0.2004':
|
||||
resolution: {integrity: sha512-7PsxuUx11Ry9DYTWcljTzRbmZY9B6O9fFFfZV+aodQEMy6pfRT3fhoQRv9PtGN/89z1pPz3nFKrsFm6WqNtX7w==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint/binding-android-arm-eabi@1.69.0':
|
||||
resolution: {integrity: sha512-DKQQbD5cZ/MYfDgDI7YGyGD9FSxABlsBsYFo5p26lloob543tP9+4N3guwdXIYJN+7HSZxLe8YJuwcOWw5qnHg==}
|
||||
'@oxlint/binding-android-arm-eabi@1.88.0':
|
||||
resolution: {integrity: sha512-k6Fgkf86ubfeH4GvqYsRolfuVAp0pfYVZvifSlXNHPdMaWUOfxUrmLEeDD7hF6U2oJuhsVxhSWq86/xCJ+9MmA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@oxlint/binding-android-arm64@1.69.0':
|
||||
resolution: {integrity: sha512-lEhb+I5pr4inux+JFwfCa1HRq3Os7NirEFQ0H1I35SVEHPm6byX0Ah47xmRha3qi6LAkxUcxViL8o/9PivjzBg==}
|
||||
'@oxlint/binding-android-arm64@1.88.0':
|
||||
resolution: {integrity: sha512-AsIyhl9o1ejwtac7NMbhKpMdSml6NWR0ljKjZERxC8OHYDAW8o/Om9n1QHVLi4VCEUo42Zte2/Op9kyoGpJZMg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@oxlint/binding-darwin-arm64@1.69.0':
|
||||
resolution: {integrity: sha512-GY2YE8lOZW59BW1Ia1y+1gR0XyjrZRvVWHAr8LGeGhYHE0OQJ/7cRKXTkx1P+E9/6awEc3SX8a68SFTjh/E//A==}
|
||||
'@oxlint/binding-darwin-arm64@1.88.0':
|
||||
resolution: {integrity: sha512-qgBjB7eEKcCB4RqjXGuoSZq74g0wD09fWFJ+HWcCJcKaga8TeUHtb+BY0SsxdleSLBgx4RZ7qmgo0EzqEq7JoQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint/binding-darwin-x64@1.69.0':
|
||||
resolution: {integrity: sha512-ax1oZnOjHX3LB7myQyHEaQkDwfLb6str3/nSP6O7EVUviQGNkEGzGV0EqcBJWK+Ufwx0l4xPgyYayurvhAdl2Q==}
|
||||
'@oxlint/binding-darwin-x64@1.88.0':
|
||||
resolution: {integrity: sha512-sCz3X5Mw/xdzEzn4fsCt1Iq8NKJnGNfhR+b9B5k+4DysYAHsmIhYoocnBPiQ9kvY3+pUun2A0Z8Vtun/Nv6YoQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint/binding-freebsd-x64@1.69.0':
|
||||
resolution: {integrity: sha512-kHWeHv4g2h8NY+mpCxzCtY4uerMJWTN/TSnNj1CPbakFpHEJ6cTya2wWV0pDSYWOJ2+0UiEbhn3AtXxHtsnKjg==}
|
||||
'@oxlint/binding-freebsd-x64@1.88.0':
|
||||
resolution: {integrity: sha512-xTOdYOIqCufgIj0knvxu86FBfovP6QBiD7ngNva01gb8xPQCD5gXkE5APGczH35zStim1O3zqvFbLQmY85xXRg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@oxlint/binding-linux-arm-gnueabihf@1.69.0':
|
||||
resolution: {integrity: sha512-gq84vM1a1oEehXo27YCDzGVcxPsZDI1yswZwz2Da1/cbnWtrL16XZZnz0G/+gIU8edtHpfjxq5c+vWEHqJfWoQ==}
|
||||
'@oxlint/binding-linux-arm-gnueabihf@1.88.0':
|
||||
resolution: {integrity: sha512-BLadtw88NdYJBrAtC1dYzrPfSP2d5zgx6iuZycdD0YmZBJ2Nqbit9Z4PSaIb6gxK25dweArGMPbOmq/0ugwUPg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/binding-linux-arm-musleabihf@1.69.0':
|
||||
resolution: {integrity: sha512-kIqEa98JQ0VRyrcncxA417m2AzasqTlD+FyVT1AksjvjkqQcvm7pBWYvoW3/mpyOP2XYvi5nSCCTIe6De1yu5g==}
|
||||
'@oxlint/binding-linux-arm-musleabihf@1.88.0':
|
||||
resolution: {integrity: sha512-wILSczl/DaZsCcf7j7tvNbssp8hxEHIJxrv4j7+JjI3TAL1U/JTWbeUtoJJC/vo8+2hkTmHLYoKJjmiNbijdXw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/binding-linux-arm64-gnu@1.69.0':
|
||||
resolution: {integrity: sha512-j+xYiXozxGWx2cpjCrwwGR4awTxPFsRv3JZrv23RCogEPMc4R7UqjHW47p/RG0aRlbWiROCJ8coUfCwy0dvzHA==}
|
||||
'@oxlint/binding-linux-arm64-gnu@1.88.0':
|
||||
resolution: {integrity: sha512-jutT2Hy8JCuZuMwIFHo2HtyxfOfBJHyQxlBHBK1SL9W2f8n/WhjAP0FnDMz9d/mjhv9+QA0mx5AdQ9fDjsrFFg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-arm64-musl@1.69.0':
|
||||
resolution: {integrity: sha512-xEPpNppTfN1l/nM7gYSf9iocscu/as+p/7vxkLeLEKnYU+09Dm+5V6IhDYDh+Uz6FajEupWwCLt5SOG0y1PCKg==}
|
||||
'@oxlint/binding-linux-arm64-musl@1.88.0':
|
||||
resolution: {integrity: sha512-8N8iGV9hh4Lkeuq7QsEkjoXNRRflqR1G2qyoGYXcA0k5qllrdqmfylKEXyHIVERxqNDyMO4StqDQ0uLxXcBQAg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.69.0':
|
||||
resolution: {integrity: sha512-Ug0+eU7HJBlek+SjklYH62IlOMirEJsdxpihH0kSqX0XdrDD4NdHpQc10fK1JC35yn6KrrcN+uYzlHD38XAf8Q==}
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.88.0':
|
||||
resolution: {integrity: sha512-5ydcQt3znNQ/rGEWYRvpS2cK9+T18P2VYgnyJ+eloEAwi9YUmsXy5VGrRqNWIK6qzWwHGnEE3ojVqqQNo+f0hA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.69.0':
|
||||
resolution: {integrity: sha512-iEyI3GIg0l/s3G4qy2TlaaWKdzj4PJJStwtlocpDTC00PY9hZueotf6OKUj9+yfQh0lrpBW/pLMgTztbAHKJEg==}
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.88.0':
|
||||
resolution: {integrity: sha512-iGx0uvFT54b/5DPJdEBCxYIrVB3v0uTy+JhRh3CyEp8/22BXDkG8rffkFwREdLsdj9mOrPbJYVOZw8ipcBuzjw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-musl@1.69.0':
|
||||
resolution: {integrity: sha512-NjHjpiI4WIKSMwuoJSZi5VToPeoYOS1FR52HLIDG6lidMdqquusgtODb4iLk0+lb1q3Z0nv2/aPRcC/olmpQGg==}
|
||||
'@oxlint/binding-linux-riscv64-musl@1.88.0':
|
||||
resolution: {integrity: sha512-Wt47mKMWelVjRjuR7BCFxXkB0m79K1sTEbqMHRWciKUgo5iv7sWsF4RXSOyFYHD9zerNrkRZblnN273rhwOA1Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-s390x-gnu@1.69.0':
|
||||
resolution: {integrity: sha512-Ai/prDewoItkDXbp38gwGZi41DycZbUTZJ3UidwoHgQC0/DaqC2TGdtBTQLJ6hSD+SAxASzh8+/eSBPmxfOacA==}
|
||||
'@oxlint/binding-linux-s390x-gnu@1.88.0':
|
||||
resolution: {integrity: sha512-g3L12KCS4+1gtUt3RbJ4F+Yxx4LNmmZLEPfhd8rAHEJjJk2lAgNfnjGoP/phJyhG5WS6VbNYbJtadJelNfvTMA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-gnu@1.69.0':
|
||||
resolution: {integrity: sha512-Gt3KHgp46mRKz4sJeaASmKvD8ayXookRw07RMf+NowhEztGGDZ7VrXpoW96XuKJLjFukWizOFVNjmYb/u7caNQ==}
|
||||
'@oxlint/binding-linux-x64-gnu@1.88.0':
|
||||
resolution: {integrity: sha512-og/XHVDOD9ZJgRiZkT9DgzQd4sr+JZW7e4l17Q/jVMZiIbeEd4uIu9iAoUbTF1gIvVZoRc4QDOxyVUvQGp4WHg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-musl@1.69.0':
|
||||
resolution: {integrity: sha512-7tQhJ2+p/oHv1zcfnjYI7YVzC/7iBaVOfIvFYtxdJ5F45mWgEdrCyXZXZGfiLey5t/5JhOhsaMnnv1kAzckd7g==}
|
||||
'@oxlint/binding-linux-x64-musl@1.88.0':
|
||||
resolution: {integrity: sha512-EmhBsp+xuKb6YbZNEMLBo80Xw71AZOIm18do8ni/GLX/Lw1XzNhVYpbxO1KasYtoAvrGtCOdCPULRlIE0bPbTQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-openharmony-arm64@1.69.0':
|
||||
resolution: {integrity: sha512-vmWz6TKp/3hfA4lksR0zHBv/6xuX1jhym6eqOjdH2DXsDDHZWcp2f0KG0VCAnlVbIrjk29G4wAWMXb/Hn1YobA==}
|
||||
'@oxlint/binding-openharmony-arm64@1.88.0':
|
||||
resolution: {integrity: sha512-DBUcx/KwYdg1DAz+G01P/GFv512J04EsZadbHptx2FTpWT/MWRg21ZToNHt9pnsfQRBQ3et/g+vqzbvrdAVbuA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@oxlint/binding-win32-arm64-msvc@1.69.0':
|
||||
resolution: {integrity: sha512-9RExaLgmaw6IoIkU9cTpT71mLfI0xZ86iZH8x518LVsOkjquJMYqb9P7KpC8lgd1t0Dxs41p2pxynq4XR3Ttzw==}
|
||||
'@oxlint/binding-win32-arm64-msvc@1.88.0':
|
||||
resolution: {integrity: sha512-xeVd0n4XG5HvynxfYvXi6uB+JvZbVZataowPS8cYpMbYg9mwE4sSyElFQd4LVJFifq8bRLDSR18XgEildmcXAg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint/binding-win32-ia32-msvc@1.69.0':
|
||||
resolution: {integrity: sha512-1907kRPF8/PrcIw1E7LMs9JbVrpgnt/MvFdss3an8oDkYNAACXzTntV3t3869ZZhMZxb2AzRGbz1pA/jdFatXA==}
|
||||
'@oxlint/binding-win32-ia32-msvc@1.88.0':
|
||||
resolution: {integrity: sha512-MA6iOw//amleaz2mVMsZrA7NHlEexjTrhIxSMFCXQi6IUuQO+3Od00GZhr2iCYw6RFzMW36jkKK/WHBXMsoQAw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint/binding-win32-x64-msvc@1.69.0':
|
||||
resolution: {integrity: sha512-w8SOXv3mT9Fi6jY8OXdXCfnvX/3KNLXGNr4HEz2TA7S4Mv/PYAOmpB8y/ge40mxvBMgGNaSaaDwZpAsQn7HtWA==}
|
||||
'@oxlint/binding-win32-x64-msvc@1.88.0':
|
||||
resolution: {integrity: sha512-3Sz6rVWe8Dy+ufsXurFN1GX2dX43F/lQMSLelK9gFjiba7nHvLlqlhR5cHlWVnCpSqUY1hN9pyCX9XToe6ve6g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -7785,29 +7785,32 @@ packages:
|
||||
oxc-resolver@11.24.2:
|
||||
resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==}
|
||||
|
||||
oxfmt@0.54.0:
|
||||
resolution: {integrity: sha512-DjnMwn7smSLF+Mc2+pRItnuPftm/dkUFpY/d4+33y9TfKrsHZo8GLhmUg9BrOIUEy94Rlom1Q11N6vuhE+e0oQ==}
|
||||
oxfmt@0.73.0:
|
||||
resolution: {integrity: sha512-Ke4xbpuzqIBP4bkd48q815RhqWNJqOBzxVA9199M4qcF0y0LtpUv7UTg1dJ5ea6jw0RBUDZvX3qOUMNmmxfxbw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@astrojs/compiler-rs': ^0.4.0 || ^0.5.0
|
||||
svelte: ^5.0.0
|
||||
vite-plus: '*'
|
||||
peerDependenciesMeta:
|
||||
'@astrojs/compiler-rs':
|
||||
optional: true
|
||||
svelte:
|
||||
optional: true
|
||||
vite-plus:
|
||||
optional: true
|
||||
|
||||
oxlint-tsgolint@0.23.0:
|
||||
resolution: {integrity: sha512-3mBv3CoPbh8dFbzfDGIWa2ytZjn2v+3EX4aKRXjIhsoGFzG8GCjfRirz3rwZf1wYbZzsNLTSgpw8VjQuWdp/jA==}
|
||||
oxlint-tsgolint@7.0.2004:
|
||||
resolution: {integrity: sha512-vQ9WDF3fiZJvRdSDosNOflerOCOpwol6vWzVDxW76RcU19cjTh7engvkHLvmv+tzedR8fAiJUVUGr6IVZdkzXQ==}
|
||||
hasBin: true
|
||||
|
||||
oxlint@1.69.0:
|
||||
resolution: {integrity: sha512-ypZkK/aDc5NQV8zIR6s2H2Tl3aNW8FmJ1m9+2qsaYuRenl8vgnHNCGwTHviWJdUQzglOlHFchgopdtGhSy17Rw==}
|
||||
oxlint@1.88.0:
|
||||
resolution: {integrity: sha512-uIcFgw8BmqvFRGzH5+r7d2zJMEexwNSrEmM6WQNnGh3VkO0XYTEFoyxyNE6tjKMUib9Cue4XJA2DoWruO5D6aw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
oxlint-tsgolint: '>=0.22.1'
|
||||
oxlint-tsgolint: '>=7.0.2003'
|
||||
vite-plus: '*'
|
||||
peerDependenciesMeta:
|
||||
oxlint-tsgolint:
|
||||
@@ -9399,8 +9402,8 @@ packages:
|
||||
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tinypool@2.1.0:
|
||||
resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==}
|
||||
tinypool@2.2.0:
|
||||
resolution: {integrity: sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==}
|
||||
engines: {node: ^20.0.0 || >=22.0.0}
|
||||
|
||||
tinyrainbow@2.0.0:
|
||||
@@ -12469,136 +12472,136 @@ snapshots:
|
||||
'@oxc-resolver/binding-win32-x64-msvc@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-android-arm-eabi@0.54.0':
|
||||
'@oxfmt/binding-android-arm-eabi@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-android-arm64@0.54.0':
|
||||
'@oxfmt/binding-android-arm64@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-darwin-arm64@0.54.0':
|
||||
'@oxfmt/binding-darwin-arm64@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-darwin-x64@0.54.0':
|
||||
'@oxfmt/binding-darwin-x64@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-freebsd-x64@0.54.0':
|
||||
'@oxfmt/binding-freebsd-x64@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-arm-gnueabihf@0.54.0':
|
||||
'@oxfmt/binding-linux-arm-gnueabihf@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-arm-musleabihf@0.54.0':
|
||||
'@oxfmt/binding-linux-arm-musleabihf@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-arm64-gnu@0.54.0':
|
||||
'@oxfmt/binding-linux-arm64-gnu@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-arm64-musl@0.54.0':
|
||||
'@oxfmt/binding-linux-arm64-musl@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.54.0':
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.54.0':
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.54.0':
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.54.0':
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-x64-gnu@0.54.0':
|
||||
'@oxfmt/binding-linux-x64-gnu@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-x64-musl@0.54.0':
|
||||
'@oxfmt/binding-linux-x64-musl@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-openharmony-arm64@0.54.0':
|
||||
'@oxfmt/binding-openharmony-arm64@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-win32-arm64-msvc@0.54.0':
|
||||
'@oxfmt/binding-win32-arm64-msvc@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-win32-ia32-msvc@0.54.0':
|
||||
'@oxfmt/binding-win32-ia32-msvc@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-win32-x64-msvc@0.54.0':
|
||||
'@oxfmt/binding-win32-x64-msvc@0.73.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@0.23.0':
|
||||
'@oxlint-tsgolint/darwin-arm64@7.0.2004':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/darwin-x64@0.23.0':
|
||||
'@oxlint-tsgolint/darwin-x64@7.0.2004':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/linux-arm64@0.23.0':
|
||||
'@oxlint-tsgolint/linux-arm64@7.0.2004':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/linux-x64@0.23.0':
|
||||
'@oxlint-tsgolint/linux-x64@7.0.2004':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/win32-arm64@0.23.0':
|
||||
'@oxlint-tsgolint/win32-arm64@7.0.2004':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/win32-x64@0.23.0':
|
||||
'@oxlint-tsgolint/win32-x64@7.0.2004':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-android-arm-eabi@1.69.0':
|
||||
'@oxlint/binding-android-arm-eabi@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-android-arm64@1.69.0':
|
||||
'@oxlint/binding-android-arm64@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-darwin-arm64@1.69.0':
|
||||
'@oxlint/binding-darwin-arm64@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-darwin-x64@1.69.0':
|
||||
'@oxlint/binding-darwin-x64@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-freebsd-x64@1.69.0':
|
||||
'@oxlint/binding-freebsd-x64@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-arm-gnueabihf@1.69.0':
|
||||
'@oxlint/binding-linux-arm-gnueabihf@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-arm-musleabihf@1.69.0':
|
||||
'@oxlint/binding-linux-arm-musleabihf@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-arm64-gnu@1.69.0':
|
||||
'@oxlint/binding-linux-arm64-gnu@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-arm64-musl@1.69.0':
|
||||
'@oxlint/binding-linux-arm64-musl@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.69.0':
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.69.0':
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-riscv64-musl@1.69.0':
|
||||
'@oxlint/binding-linux-riscv64-musl@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-s390x-gnu@1.69.0':
|
||||
'@oxlint/binding-linux-s390x-gnu@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-x64-gnu@1.69.0':
|
||||
'@oxlint/binding-linux-x64-gnu@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-x64-musl@1.69.0':
|
||||
'@oxlint/binding-linux-x64-musl@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-openharmony-arm64@1.69.0':
|
||||
'@oxlint/binding-openharmony-arm64@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-win32-arm64-msvc@1.69.0':
|
||||
'@oxlint/binding-win32-arm64-msvc@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-win32-ia32-msvc@1.69.0':
|
||||
'@oxlint/binding-win32-ia32-msvc@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-win32-x64-msvc@1.69.0':
|
||||
'@oxlint/binding-win32-x64-msvc@1.88.0':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.1':
|
||||
@@ -18323,61 +18326,61 @@ snapshots:
|
||||
'@oxc-resolver/binding-win32-arm64-msvc': 11.24.2
|
||||
'@oxc-resolver/binding-win32-x64-msvc': 11.24.2
|
||||
|
||||
oxfmt@0.54.0:
|
||||
oxfmt@0.73.0:
|
||||
dependencies:
|
||||
tinypool: 2.1.0
|
||||
tinypool: 2.2.0
|
||||
optionalDependencies:
|
||||
'@oxfmt/binding-android-arm-eabi': 0.54.0
|
||||
'@oxfmt/binding-android-arm64': 0.54.0
|
||||
'@oxfmt/binding-darwin-arm64': 0.54.0
|
||||
'@oxfmt/binding-darwin-x64': 0.54.0
|
||||
'@oxfmt/binding-freebsd-x64': 0.54.0
|
||||
'@oxfmt/binding-linux-arm-gnueabihf': 0.54.0
|
||||
'@oxfmt/binding-linux-arm-musleabihf': 0.54.0
|
||||
'@oxfmt/binding-linux-arm64-gnu': 0.54.0
|
||||
'@oxfmt/binding-linux-arm64-musl': 0.54.0
|
||||
'@oxfmt/binding-linux-ppc64-gnu': 0.54.0
|
||||
'@oxfmt/binding-linux-riscv64-gnu': 0.54.0
|
||||
'@oxfmt/binding-linux-riscv64-musl': 0.54.0
|
||||
'@oxfmt/binding-linux-s390x-gnu': 0.54.0
|
||||
'@oxfmt/binding-linux-x64-gnu': 0.54.0
|
||||
'@oxfmt/binding-linux-x64-musl': 0.54.0
|
||||
'@oxfmt/binding-openharmony-arm64': 0.54.0
|
||||
'@oxfmt/binding-win32-arm64-msvc': 0.54.0
|
||||
'@oxfmt/binding-win32-ia32-msvc': 0.54.0
|
||||
'@oxfmt/binding-win32-x64-msvc': 0.54.0
|
||||
'@oxfmt/binding-android-arm-eabi': 0.73.0
|
||||
'@oxfmt/binding-android-arm64': 0.73.0
|
||||
'@oxfmt/binding-darwin-arm64': 0.73.0
|
||||
'@oxfmt/binding-darwin-x64': 0.73.0
|
||||
'@oxfmt/binding-freebsd-x64': 0.73.0
|
||||
'@oxfmt/binding-linux-arm-gnueabihf': 0.73.0
|
||||
'@oxfmt/binding-linux-arm-musleabihf': 0.73.0
|
||||
'@oxfmt/binding-linux-arm64-gnu': 0.73.0
|
||||
'@oxfmt/binding-linux-arm64-musl': 0.73.0
|
||||
'@oxfmt/binding-linux-ppc64-gnu': 0.73.0
|
||||
'@oxfmt/binding-linux-riscv64-gnu': 0.73.0
|
||||
'@oxfmt/binding-linux-riscv64-musl': 0.73.0
|
||||
'@oxfmt/binding-linux-s390x-gnu': 0.73.0
|
||||
'@oxfmt/binding-linux-x64-gnu': 0.73.0
|
||||
'@oxfmt/binding-linux-x64-musl': 0.73.0
|
||||
'@oxfmt/binding-openharmony-arm64': 0.73.0
|
||||
'@oxfmt/binding-win32-arm64-msvc': 0.73.0
|
||||
'@oxfmt/binding-win32-ia32-msvc': 0.73.0
|
||||
'@oxfmt/binding-win32-x64-msvc': 0.73.0
|
||||
|
||||
oxlint-tsgolint@0.23.0:
|
||||
oxlint-tsgolint@7.0.2004:
|
||||
optionalDependencies:
|
||||
'@oxlint-tsgolint/darwin-arm64': 0.23.0
|
||||
'@oxlint-tsgolint/darwin-x64': 0.23.0
|
||||
'@oxlint-tsgolint/linux-arm64': 0.23.0
|
||||
'@oxlint-tsgolint/linux-x64': 0.23.0
|
||||
'@oxlint-tsgolint/win32-arm64': 0.23.0
|
||||
'@oxlint-tsgolint/win32-x64': 0.23.0
|
||||
'@oxlint-tsgolint/darwin-arm64': 7.0.2004
|
||||
'@oxlint-tsgolint/darwin-x64': 7.0.2004
|
||||
'@oxlint-tsgolint/linux-arm64': 7.0.2004
|
||||
'@oxlint-tsgolint/linux-x64': 7.0.2004
|
||||
'@oxlint-tsgolint/win32-arm64': 7.0.2004
|
||||
'@oxlint-tsgolint/win32-x64': 7.0.2004
|
||||
|
||||
oxlint@1.69.0(oxlint-tsgolint@0.23.0):
|
||||
oxlint@1.88.0(oxlint-tsgolint@7.0.2004):
|
||||
optionalDependencies:
|
||||
'@oxlint/binding-android-arm-eabi': 1.69.0
|
||||
'@oxlint/binding-android-arm64': 1.69.0
|
||||
'@oxlint/binding-darwin-arm64': 1.69.0
|
||||
'@oxlint/binding-darwin-x64': 1.69.0
|
||||
'@oxlint/binding-freebsd-x64': 1.69.0
|
||||
'@oxlint/binding-linux-arm-gnueabihf': 1.69.0
|
||||
'@oxlint/binding-linux-arm-musleabihf': 1.69.0
|
||||
'@oxlint/binding-linux-arm64-gnu': 1.69.0
|
||||
'@oxlint/binding-linux-arm64-musl': 1.69.0
|
||||
'@oxlint/binding-linux-ppc64-gnu': 1.69.0
|
||||
'@oxlint/binding-linux-riscv64-gnu': 1.69.0
|
||||
'@oxlint/binding-linux-riscv64-musl': 1.69.0
|
||||
'@oxlint/binding-linux-s390x-gnu': 1.69.0
|
||||
'@oxlint/binding-linux-x64-gnu': 1.69.0
|
||||
'@oxlint/binding-linux-x64-musl': 1.69.0
|
||||
'@oxlint/binding-openharmony-arm64': 1.69.0
|
||||
'@oxlint/binding-win32-arm64-msvc': 1.69.0
|
||||
'@oxlint/binding-win32-ia32-msvc': 1.69.0
|
||||
'@oxlint/binding-win32-x64-msvc': 1.69.0
|
||||
oxlint-tsgolint: 0.23.0
|
||||
'@oxlint/binding-android-arm-eabi': 1.88.0
|
||||
'@oxlint/binding-android-arm64': 1.88.0
|
||||
'@oxlint/binding-darwin-arm64': 1.88.0
|
||||
'@oxlint/binding-darwin-x64': 1.88.0
|
||||
'@oxlint/binding-freebsd-x64': 1.88.0
|
||||
'@oxlint/binding-linux-arm-gnueabihf': 1.88.0
|
||||
'@oxlint/binding-linux-arm-musleabihf': 1.88.0
|
||||
'@oxlint/binding-linux-arm64-gnu': 1.88.0
|
||||
'@oxlint/binding-linux-arm64-musl': 1.88.0
|
||||
'@oxlint/binding-linux-ppc64-gnu': 1.88.0
|
||||
'@oxlint/binding-linux-riscv64-gnu': 1.88.0
|
||||
'@oxlint/binding-linux-riscv64-musl': 1.88.0
|
||||
'@oxlint/binding-linux-s390x-gnu': 1.88.0
|
||||
'@oxlint/binding-linux-x64-gnu': 1.88.0
|
||||
'@oxlint/binding-linux-x64-musl': 1.88.0
|
||||
'@oxlint/binding-openharmony-arm64': 1.88.0
|
||||
'@oxlint/binding-win32-arm64-msvc': 1.88.0
|
||||
'@oxlint/binding-win32-ia32-msvc': 1.88.0
|
||||
'@oxlint/binding-win32-x64-msvc': 1.88.0
|
||||
oxlint-tsgolint: 7.0.2004
|
||||
|
||||
p-limit@2.3.0:
|
||||
dependencies:
|
||||
@@ -20206,7 +20209,7 @@ snapshots:
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
|
||||
tinypool@2.1.0: {}
|
||||
tinypool@2.2.0: {}
|
||||
|
||||
tinyrainbow@2.0.0: {}
|
||||
|
||||
@@ -20588,7 +20591,7 @@ snapshots:
|
||||
unist-util-stringify-position: 4.0.0
|
||||
vfile-message: 4.0.2
|
||||
|
||||
vite-plugin-checker@0.12.0(eslint@10.2.1(jiti@2.6.1))(meow@13.2.0)(optionator@0.9.4)(oxlint@1.69.0(oxlint-tsgolint@0.23.0))(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(stylelint@17.15.0(typescript@5.9.3))(typescript@5.9.3):
|
||||
vite-plugin-checker@0.12.0(eslint@10.2.1(jiti@2.6.1))(meow@13.2.0)(optionator@0.9.4)(oxlint@1.88.0(oxlint-tsgolint@7.0.2004))(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(stylelint@17.15.0(typescript@5.9.3))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
chokidar: 4.0.3
|
||||
@@ -20603,7 +20606,7 @@ snapshots:
|
||||
eslint: 10.2.1(jiti@2.6.1)
|
||||
meow: 13.2.0
|
||||
optionator: 0.9.4
|
||||
oxlint: 1.69.0(oxlint-tsgolint@0.23.0)
|
||||
oxlint: 1.88.0(oxlint-tsgolint@7.0.2004)
|
||||
stylelint: 17.15.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
|
||||
|
||||
@@ -300,20 +300,17 @@ describe('PrivateRoute', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should redirect /logs-save-views to /logs/saved-views', () => {
|
||||
renderPrivateRoute({ initialRoute: '/logs-save-views' });
|
||||
it.each([
|
||||
['/logs-save-views', ROUTES.LOGS_EXPLORER],
|
||||
['/traces-save-views', ROUTES.TRACES_EXPLORER],
|
||||
[ROUTES.LOGS_SAVE_VIEWS, ROUTES.LOGS_EXPLORER],
|
||||
[ROUTES.TRACES_SAVE_VIEWS, ROUTES.TRACES_EXPLORER],
|
||||
[ROUTES.METRICS_EXPLORER_VIEWS, ROUTES.METRICS_EXPLORER_EXPLORER],
|
||||
[ROUTES.METER_EXPLORER_VIEWS, ROUTES.METER_EXPLORER],
|
||||
])('should redirect the old views route %s to %s', (from, to) => {
|
||||
renderPrivateRoute({ initialRoute: from });
|
||||
|
||||
expect(screen.getByTestId('location-display')).toHaveTextContent(
|
||||
'/logs/saved-views',
|
||||
);
|
||||
});
|
||||
|
||||
it('should redirect /traces-save-views to /traces/saved-views', () => {
|
||||
renderPrivateRoute({ initialRoute: '/traces-save-views' });
|
||||
|
||||
expect(screen.getByTestId('location-display')).toHaveTextContent(
|
||||
'/traces/saved-views',
|
||||
);
|
||||
expect(screen.getByTestId('location-display').textContent).toBe(to);
|
||||
});
|
||||
|
||||
it('should redirect /settings/access-tokens to /settings/service-accounts', () => {
|
||||
@@ -1635,11 +1632,6 @@ describe('PrivateRoute', () => {
|
||||
['LIST_LICENSES', ROUTES.LIST_LICENSES, USER_ROLES.EDITOR as ROLES],
|
||||
['ONBOARDING', ROUTES.ONBOARDING, USER_ROLES.VIEWER as ROLES],
|
||||
['APPLICATION', ROUTES.APPLICATION, USER_ROLES.ANONYMOUS as ROLES],
|
||||
[
|
||||
'METRICS_EXPLORER_VIEWS',
|
||||
ROUTES.METRICS_EXPLORER_VIEWS,
|
||||
USER_ROLES.ANONYMOUS as ROLES,
|
||||
],
|
||||
];
|
||||
|
||||
it.each(authzRouteRolePairs)(
|
||||
|
||||
@@ -513,8 +513,12 @@ export const LIST_LICENSES: AppRoutes = {
|
||||
export const oldNewRoutesMapping: Record<string, string> = {
|
||||
'/pipelines': '/logs/pipelines',
|
||||
'/logs-explorer': '/logs/logs-explorer',
|
||||
'/logs-save-views': '/logs/saved-views',
|
||||
'/traces-save-views': '/traces/saved-views',
|
||||
'/logs-save-views': '/logs/logs-explorer',
|
||||
'/traces-save-views': '/traces-explorer',
|
||||
'/logs/saved-views': '/logs/logs-explorer',
|
||||
'/traces/saved-views': '/traces-explorer',
|
||||
'/metrics-explorer/views': '/metrics-explorer/explorer',
|
||||
'/meter/explorer/views': '/meter/explorer',
|
||||
'/settings/access-tokens': '/settings/service-accounts',
|
||||
'/settings/api-keys': '/settings/service-accounts',
|
||||
'/messaging-queues': '/messaging-queues/overview',
|
||||
|
||||
@@ -220,8 +220,7 @@ export type ClarificationSummaryDTODiscoveredContextAnyOf = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type ClarificationSummaryDTODiscoveredContext =
|
||||
ClarificationSummaryDTODiscoveredContextAnyOf | null;
|
||||
export type ClarificationSummaryDTODiscoveredContext = ClarificationSummaryDTODiscoveredContextAnyOf | null;
|
||||
|
||||
export interface ClarificationSummaryDTO {
|
||||
/**
|
||||
@@ -784,8 +783,7 @@ export type ThreadDetailResponseDTOActiveExecutionId = string | null;
|
||||
|
||||
export type ThreadDetailResponseDTOPendingApproval = ApprovalSummaryDTO | null;
|
||||
|
||||
export type ThreadDetailResponseDTOPendingClarification =
|
||||
ClarificationSummaryDTO | null;
|
||||
export type ThreadDetailResponseDTOPendingClarification = ClarificationSummaryDTO | null;
|
||||
|
||||
/**
|
||||
* Full thread state for cold-start reconstruction (GET /threads/{id}).
|
||||
@@ -975,8 +973,7 @@ export type ClarificationEventDTODiscoveredContextAnyOf = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type ClarificationEventDTODiscoveredContext =
|
||||
ClarificationEventDTODiscoveredContextAnyOf | null;
|
||||
export type ClarificationEventDTODiscoveredContext = ClarificationEventDTODiscoveredContextAnyOf | null;
|
||||
|
||||
export interface ClarificationEventDTO {
|
||||
/**
|
||||
@@ -1299,8 +1296,7 @@ export type CreateThreadApiV1AssistantThreadsPostHeaders = {
|
||||
'X-SigNoz-URL'?: string | null;
|
||||
};
|
||||
|
||||
export type CreateThreadApiV1AssistantThreadsPostBody =
|
||||
CreateThreadRequestDTO | null;
|
||||
export type CreateThreadApiV1AssistantThreadsPostBody = CreateThreadRequestDTO | null;
|
||||
|
||||
export type ListThreadsApiV1AssistantThreadsGetParams = {
|
||||
/**
|
||||
|
||||
@@ -431,8 +431,7 @@ export type AlertmanagertypesChannelEmailConfigDTOHeadersAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type AlertmanagertypesChannelEmailConfigDTOHeaders =
|
||||
AlertmanagertypesChannelEmailConfigDTOHeadersAnyOf | null;
|
||||
export type AlertmanagertypesChannelEmailConfigDTOHeaders = AlertmanagertypesChannelEmailConfigDTOHeadersAnyOf | null;
|
||||
|
||||
export interface AlertmanagertypesChannelEmailConfigDTO {
|
||||
/**
|
||||
@@ -510,8 +509,7 @@ export type AlertmanagertypesChannelPagerdutyConfigDTODetailsAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type AlertmanagertypesChannelPagerdutyConfigDTODetails =
|
||||
AlertmanagertypesChannelPagerdutyConfigDTODetailsAnyOf | null;
|
||||
export type AlertmanagertypesChannelPagerdutyConfigDTODetails = AlertmanagertypesChannelPagerdutyConfigDTODetailsAnyOf | null;
|
||||
|
||||
export interface AlertmanagertypesChannelPagerdutyConfigDTO {
|
||||
/**
|
||||
@@ -584,8 +582,7 @@ export type AlertmanagertypesChannelOpsgenieConfigDTODetailsAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type AlertmanagertypesChannelOpsgenieConfigDTODetails =
|
||||
AlertmanagertypesChannelOpsgenieConfigDTODetailsAnyOf | null;
|
||||
export type AlertmanagertypesChannelOpsgenieConfigDTODetails = AlertmanagertypesChannelOpsgenieConfigDTODetailsAnyOf | null;
|
||||
|
||||
export interface AlertmanagertypesChannelOpsgenieConfigDTO {
|
||||
/**
|
||||
@@ -706,8 +703,7 @@ export type AlertmanagertypesChannelJiraConfigDTOCustomFieldsAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type AlertmanagertypesChannelJiraConfigDTOCustomFields =
|
||||
AlertmanagertypesChannelJiraConfigDTOCustomFieldsAnyOf | null;
|
||||
export type AlertmanagertypesChannelJiraConfigDTOCustomFields = AlertmanagertypesChannelJiraConfigDTOCustomFieldsAnyOf | null;
|
||||
|
||||
export interface AlertmanagertypesChannelJiraConfigDTO {
|
||||
/**
|
||||
@@ -832,8 +828,7 @@ export type AlertmanagertypesChannelIncidentIOConfigDTOMetadataAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type AlertmanagertypesChannelIncidentIOConfigDTOMetadata =
|
||||
AlertmanagertypesChannelIncidentIOConfigDTOMetadataAnyOf | null;
|
||||
export type AlertmanagertypesChannelIncidentIOConfigDTOMetadata = AlertmanagertypesChannelIncidentIOConfigDTOMetadataAnyOf | null;
|
||||
|
||||
export interface AlertmanagertypesChannelIncidentIOConfigDTO {
|
||||
/**
|
||||
@@ -872,6 +867,55 @@ export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTy
|
||||
spec: AlertmanagertypesChannelIncidentIOConfigDTO;
|
||||
}
|
||||
|
||||
export enum AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelTelegramConfigDTOKind {
|
||||
telegram = 'telegram',
|
||||
}
|
||||
export interface AlertmanagertypesChannelTelegramConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
apiUrl?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format password
|
||||
*/
|
||||
botToken: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
chatId: number;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
disableNotifications?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
message?: string;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
messageThreadId?: number;
|
||||
/**
|
||||
* @type boolean,null
|
||||
*/
|
||||
sendResolved?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelTelegramConfigDTO {
|
||||
/**
|
||||
* @enum telegram
|
||||
* @type string
|
||||
*/
|
||||
kind: AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelTelegramConfigDTOKind;
|
||||
spec: AlertmanagertypesChannelTelegramConfigDTO;
|
||||
}
|
||||
|
||||
export type AlertmanagertypesChannelConfigDTO =
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelSlackConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelEmailConfigDTO
|
||||
@@ -882,7 +926,8 @@ export type AlertmanagertypesChannelConfigDTO =
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelGoogleChatConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJiraConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJSMOpsConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelIncidentIOConfigDTO;
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelIncidentIOConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelTelegramConfigDTO;
|
||||
|
||||
export enum AlertmanagertypesChannelDefectDTO {
|
||||
none = 'none',
|
||||
@@ -902,6 +947,7 @@ export enum AlertmanagertypesChannelKindDTO {
|
||||
jira = 'jira',
|
||||
jsmops = 'jsmops',
|
||||
incidentio = 'incidentio',
|
||||
telegram = 'telegram',
|
||||
}
|
||||
export enum AlertmanagertypesChannelListOrderDTO {
|
||||
asc = 'asc',
|
||||
@@ -2462,7 +2508,7 @@ export interface ConfigSNSConfigDTO {
|
||||
topic_arn?: string;
|
||||
}
|
||||
|
||||
export interface ConfigTelegramConfigDTO {
|
||||
export interface AlertmanagertypesTelegramReceiverConfigDTO {
|
||||
api_url?: ConfigURLType2DTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -2494,6 +2540,10 @@ export interface ConfigTelegramConfigDTO {
|
||||
* @type boolean
|
||||
*/
|
||||
send_resolved?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2699,7 +2749,7 @@ export type AlertmanagertypesPostableChannelDTO = unknown & {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
telegram_configs?: ConfigTelegramConfigDTO[];
|
||||
telegram_configs?: AlertmanagertypesTelegramReceiverConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
@@ -2846,7 +2896,7 @@ export interface AlertmanagertypesReceiverDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
telegram_configs?: ConfigTelegramConfigDTO[];
|
||||
telegram_configs?: AlertmanagertypesTelegramReceiverConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
@@ -2899,6 +2949,9 @@ export interface AuthtypesAttributeMappingDTO {
|
||||
role?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The kind of authn provider.
|
||||
*/
|
||||
export enum AuthtypesAuthDomainConfigSAMLDTOKind {
|
||||
saml = 'saml',
|
||||
}
|
||||
@@ -2926,11 +2979,18 @@ export interface AuthtypesAuthDomainConfigSAMLDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum saml
|
||||
* @description The kind of authn provider.
|
||||
*/
|
||||
kind: AuthtypesAuthDomainConfigSAMLDTOKind;
|
||||
/**
|
||||
* @description The saml configuration.
|
||||
*/
|
||||
spec: AuthtypesSamlConfigDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* The kind of authn provider.
|
||||
*/
|
||||
export enum AuthtypesAuthDomainConfigGoogleDTOKind {
|
||||
google = 'google',
|
||||
}
|
||||
@@ -2979,11 +3039,18 @@ export interface AuthtypesAuthDomainConfigGoogleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum google
|
||||
* @description The kind of authn provider.
|
||||
*/
|
||||
kind: AuthtypesAuthDomainConfigGoogleDTOKind;
|
||||
/**
|
||||
* @description The google auth configuration.
|
||||
*/
|
||||
spec: AuthtypesGoogleConfigDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* The kind of authn provider.
|
||||
*/
|
||||
export enum AuthtypesAuthDomainConfigOIDCDTOKind {
|
||||
oidc = 'oidc',
|
||||
}
|
||||
@@ -3020,8 +3087,12 @@ export interface AuthtypesAuthDomainConfigOIDCDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum oidc
|
||||
* @description The kind of authn provider.
|
||||
*/
|
||||
kind: AuthtypesAuthDomainConfigOIDCDTOKind;
|
||||
/**
|
||||
* @description The oidc configuration.
|
||||
*/
|
||||
spec: AuthtypesOIDCConfigDTO;
|
||||
}
|
||||
|
||||
@@ -3080,8 +3151,7 @@ export type AuthtypesRoleMappingDTOGroupMappingsAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type AuthtypesRoleMappingDTOGroupMappings =
|
||||
AuthtypesRoleMappingDTOGroupMappingsAnyOf | null;
|
||||
export type AuthtypesRoleMappingDTOGroupMappings = AuthtypesRoleMappingDTOGroupMappingsAnyOf | null;
|
||||
|
||||
export interface AuthtypesRoleMappingDTO {
|
||||
/**
|
||||
@@ -3692,8 +3762,7 @@ export type CloudintegrationtypesAgentReportDTODataAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type CloudintegrationtypesAgentReportDTOData =
|
||||
CloudintegrationtypesAgentReportDTODataAnyOf | null;
|
||||
export type CloudintegrationtypesAgentReportDTOData = CloudintegrationtypesAgentReportDTODataAnyOf | null;
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
@@ -4179,8 +4248,7 @@ export type CloudintegrationtypesPostableAgentCheckInDTODataAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type CloudintegrationtypesPostableAgentCheckInDTOData =
|
||||
CloudintegrationtypesPostableAgentCheckInDTODataAnyOf | null;
|
||||
export type CloudintegrationtypesPostableAgentCheckInDTOData = CloudintegrationtypesPostableAgentCheckInDTODataAnyOf | null;
|
||||
|
||||
export interface CloudintegrationtypesPostableAgentCheckInDTO {
|
||||
/**
|
||||
@@ -4422,6 +4490,48 @@ export interface ConfigJiraConfigDTO {
|
||||
wont_fix_resolution?: string;
|
||||
}
|
||||
|
||||
export interface ConfigTelegramConfigDTO {
|
||||
api_url?: ConfigURLType2DTO;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
chat?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
chat_file?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
disable_notifications?: boolean;
|
||||
http_config?: ConfigHTTPClientConfigDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
message?: string;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
message_thread_id?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
parse_mode?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
send_resolved?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
token?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
token_file?: string;
|
||||
}
|
||||
|
||||
export interface DashboardGridItemDTO {
|
||||
content?: CommonJSONRefDTO;
|
||||
/**
|
||||
@@ -4483,6 +4593,9 @@ export interface Querybuildertypesv5TraceAggregationDTO {
|
||||
expression?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* How the upper bounds are spaced.
|
||||
*/
|
||||
export enum Querybuildertypesv5BucketOptionsLinearDTOKind {
|
||||
linear = 'linear',
|
||||
}
|
||||
@@ -4502,11 +4615,18 @@ export interface Querybuildertypesv5BucketOptionsLinearDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum linear
|
||||
* @description How the upper bounds are spaced.
|
||||
*/
|
||||
kind: Querybuildertypesv5BucketOptionsLinearDTOKind;
|
||||
/**
|
||||
* @description The evenly spaced bucket specification.
|
||||
*/
|
||||
spec: Querybuildertypesv5LinearBucketsSpecDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* How the upper bounds are spaced.
|
||||
*/
|
||||
export enum Querybuildertypesv5BucketOptionsLogDTOKind {
|
||||
log = 'log',
|
||||
}
|
||||
@@ -4521,8 +4641,12 @@ export interface Querybuildertypesv5BucketOptionsLogDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum log
|
||||
* @description How the upper bounds are spaced.
|
||||
*/
|
||||
kind: Querybuildertypesv5BucketOptionsLogDTOKind;
|
||||
/**
|
||||
* @description The logarithmic bucket specification.
|
||||
*/
|
||||
spec: Querybuildertypesv5LogBucketsSpecDTO;
|
||||
}
|
||||
|
||||
@@ -4868,8 +4992,7 @@ export type DashboardtypesLegendDTOCustomColorsAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type DashboardtypesLegendDTOCustomColors =
|
||||
DashboardtypesLegendDTOCustomColorsAnyOf | null;
|
||||
export type DashboardtypesLegendDTOCustomColors = DashboardtypesLegendDTOCustomColorsAnyOf | null;
|
||||
|
||||
export interface DashboardtypesLegendDTO {
|
||||
/**
|
||||
@@ -5461,8 +5584,7 @@ export type DashboardtypesTableFormattingDTOColumnUnitsAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type DashboardtypesTableFormattingDTOColumnUnits =
|
||||
DashboardtypesTableFormattingDTOColumnUnitsAnyOf | null;
|
||||
export type DashboardtypesTableFormattingDTOColumnUnits = DashboardtypesTableFormattingDTOColumnUnitsAnyOf | null;
|
||||
|
||||
export interface DashboardtypesTableFormattingDTO {
|
||||
/**
|
||||
@@ -5747,26 +5869,40 @@ export type Querybuildertypesv5BuilderQuerySpecDTO =
|
||||
| Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregationDTO
|
||||
| Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregationDTO;
|
||||
|
||||
/**
|
||||
* The type of the query.
|
||||
*/
|
||||
export enum Querybuildertypesv5QueryEnvelopeBuilderDTOType {
|
||||
builder_query = 'builder_query',
|
||||
}
|
||||
export interface Querybuildertypesv5QueryEnvelopeBuilderDTO {
|
||||
/**
|
||||
* @description The builder query specification.
|
||||
*/
|
||||
spec?: Querybuildertypesv5BuilderQuerySpecDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @enum builder_query
|
||||
* @description The type of the query.
|
||||
*/
|
||||
type: Querybuildertypesv5QueryEnvelopeBuilderDTOType;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the query.
|
||||
*/
|
||||
export enum Querybuildertypesv5QueryEnvelopeBuilderAIDTOType {
|
||||
builder_ai_query = 'builder_ai_query',
|
||||
}
|
||||
export interface Querybuildertypesv5QueryEnvelopeBuilderAIDTO {
|
||||
/**
|
||||
* @description The AI builder query specification.
|
||||
*/
|
||||
spec?: Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregationDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @enum builder_ai_query
|
||||
* @description The type of the query.
|
||||
*/
|
||||
type: Querybuildertypesv5QueryEnvelopeBuilderAIDTOType;
|
||||
}
|
||||
@@ -5804,14 +5940,21 @@ export interface Querybuildertypesv5QueryBuilderFormulaDTO {
|
||||
order?: Querybuildertypesv5OrderByDTO[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the query.
|
||||
*/
|
||||
export enum Querybuildertypesv5QueryEnvelopeFormulaDTOType {
|
||||
builder_formula = 'builder_formula',
|
||||
}
|
||||
export interface Querybuildertypesv5QueryEnvelopeFormulaDTO {
|
||||
/**
|
||||
* @description The formula specification.
|
||||
*/
|
||||
spec?: Querybuildertypesv5QueryBuilderFormulaDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @enum builder_formula
|
||||
* @description The type of the query.
|
||||
*/
|
||||
type: Querybuildertypesv5QueryEnvelopeFormulaDTOType;
|
||||
}
|
||||
@@ -5874,14 +6017,21 @@ export interface Querybuildertypesv5QueryBuilderTraceOperatorDTO {
|
||||
stepInterval?: Querybuildertypesv5StepDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the query.
|
||||
*/
|
||||
export enum Querybuildertypesv5QueryEnvelopeTraceOperatorDTOType {
|
||||
builder_trace_operator = 'builder_trace_operator',
|
||||
}
|
||||
export interface Querybuildertypesv5QueryEnvelopeTraceOperatorDTO {
|
||||
/**
|
||||
* @description The trace operator specification.
|
||||
*/
|
||||
spec?: Querybuildertypesv5QueryBuilderTraceOperatorDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @enum builder_trace_operator
|
||||
* @description The type of the query.
|
||||
*/
|
||||
type: Querybuildertypesv5QueryEnvelopeTraceOperatorDTOType;
|
||||
}
|
||||
@@ -5910,14 +6060,21 @@ export interface Querybuildertypesv5PromQueryDTO {
|
||||
step?: Querybuildertypesv5StepDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the query.
|
||||
*/
|
||||
export enum Querybuildertypesv5QueryEnvelopePromQLDTOType {
|
||||
promql = 'promql',
|
||||
}
|
||||
export interface Querybuildertypesv5QueryEnvelopePromQLDTO {
|
||||
/**
|
||||
* @description The PromQL query specification.
|
||||
*/
|
||||
spec?: Querybuildertypesv5PromQueryDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @enum promql
|
||||
* @description The type of the query.
|
||||
*/
|
||||
type: Querybuildertypesv5QueryEnvelopePromQLDTOType;
|
||||
}
|
||||
@@ -5941,14 +6098,21 @@ export interface Querybuildertypesv5ClickHouseQueryDTO {
|
||||
query?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the query.
|
||||
*/
|
||||
export enum Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType {
|
||||
clickhouse_sql = 'clickhouse_sql',
|
||||
}
|
||||
export interface Querybuildertypesv5QueryEnvelopeClickHouseSQLDTO {
|
||||
/**
|
||||
* @description The ClickHouse SQL query specification.
|
||||
*/
|
||||
spec?: Querybuildertypesv5ClickHouseQueryDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @enum clickhouse_sql
|
||||
* @description The type of the query.
|
||||
*/
|
||||
type: Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType;
|
||||
}
|
||||
@@ -6777,8 +6941,7 @@ export type FeaturetypesGettableFeatureDTOVariantsAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type FeaturetypesGettableFeatureDTOVariants =
|
||||
FeaturetypesGettableFeatureDTOVariantsAnyOf | null;
|
||||
export type FeaturetypesGettableFeatureDTOVariants = FeaturetypesGettableFeatureDTOVariantsAnyOf | null;
|
||||
|
||||
export interface FeaturetypesGettableFeatureDTO {
|
||||
/**
|
||||
@@ -7256,8 +7419,7 @@ export type InframonitoringtypesClusterRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesClusterRecordDTOMeta =
|
||||
InframonitoringtypesClusterRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesClusterRecordDTOMeta = InframonitoringtypesClusterRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesNodeCountsByReadinessDTO {
|
||||
/**
|
||||
@@ -7530,8 +7692,7 @@ export type InframonitoringtypesContainerRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesContainerRecordDTOMeta =
|
||||
InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesContainerRecordDTOMeta = InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesContainerRecordDTO {
|
||||
containerCountsByReady: InframonitoringtypesContainerCountsByReadyDTO;
|
||||
@@ -7622,8 +7783,7 @@ export type InframonitoringtypesDaemonSetRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesDaemonSetRecordDTOMeta =
|
||||
InframonitoringtypesDaemonSetRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesDaemonSetRecordDTOMeta = InframonitoringtypesDaemonSetRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesDaemonSetRecordDTO {
|
||||
/**
|
||||
@@ -7718,8 +7878,7 @@ export type InframonitoringtypesDeploymentRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesDeploymentRecordDTOMeta =
|
||||
InframonitoringtypesDeploymentRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesDeploymentRecordDTOMeta = InframonitoringtypesDeploymentRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesDeploymentRecordDTO {
|
||||
/**
|
||||
@@ -7808,8 +7967,7 @@ export type InframonitoringtypesHostRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesHostRecordDTOMeta =
|
||||
InframonitoringtypesHostRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesHostRecordDTOMeta = InframonitoringtypesHostRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesHostRecordDTO {
|
||||
/**
|
||||
@@ -7891,8 +8049,7 @@ export type InframonitoringtypesJobRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesJobRecordDTOMeta =
|
||||
InframonitoringtypesJobRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesJobRecordDTOMeta = InframonitoringtypesJobRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesJobRecordDTO {
|
||||
/**
|
||||
@@ -8010,8 +8167,7 @@ export type InframonitoringtypesNamespaceRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesNamespaceRecordDTOMeta =
|
||||
InframonitoringtypesNamespaceRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesNamespaceRecordDTOMeta = InframonitoringtypesNamespaceRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesNamespaceRecordDTO {
|
||||
/**
|
||||
@@ -8078,8 +8234,7 @@ export type InframonitoringtypesNodeRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesNodeRecordDTOMeta =
|
||||
InframonitoringtypesNodeRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesNodeRecordDTOMeta = InframonitoringtypesNodeRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesNodeRecordDTO {
|
||||
condition: InframonitoringtypesNodeConditionDTO;
|
||||
@@ -8150,8 +8305,7 @@ export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesPodRecordDTOMeta =
|
||||
InframonitoringtypesPodRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesPodRecordDTOMeta = InframonitoringtypesPodRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesPodRecordDTO {
|
||||
/**
|
||||
@@ -8538,8 +8692,7 @@ export type InframonitoringtypesStatefulSetRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesStatefulSetRecordDTOMeta =
|
||||
InframonitoringtypesStatefulSetRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesStatefulSetRecordDTOMeta = InframonitoringtypesStatefulSetRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesStatefulSetRecordDTO {
|
||||
/**
|
||||
@@ -8615,8 +8768,7 @@ export type InframonitoringtypesVolumeRecordDTOMetaAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesVolumeRecordDTOMeta =
|
||||
InframonitoringtypesVolumeRecordDTOMetaAnyOf | null;
|
||||
export type InframonitoringtypesVolumeRecordDTOMeta = InframonitoringtypesVolumeRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesVolumeRecordDTO {
|
||||
/**
|
||||
@@ -9734,8 +9886,7 @@ export type PrometheusMatrixSeriesSchemaDTOMetricAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type PrometheusMatrixSeriesSchemaDTOMetric =
|
||||
PrometheusMatrixSeriesSchemaDTOMetricAnyOf | null;
|
||||
export type PrometheusMatrixSeriesSchemaDTOMetric = PrometheusMatrixSeriesSchemaDTOMetricAnyOf | null;
|
||||
|
||||
export interface PrometheusMatrixSeriesSchemaDTO {
|
||||
/**
|
||||
@@ -9767,8 +9918,7 @@ export type PrometheusVectorSampleSchemaDTOMetricAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type PrometheusVectorSampleSchemaDTOMetric =
|
||||
PrometheusVectorSampleSchemaDTOMetricAnyOf | null;
|
||||
export type PrometheusVectorSampleSchemaDTOMetric = PrometheusVectorSampleSchemaDTOMetricAnyOf | null;
|
||||
|
||||
export interface PrometheusVectorSampleSchemaDTO {
|
||||
/**
|
||||
@@ -10178,8 +10328,7 @@ export type Querybuildertypesv5RawRowDTODataAnyOf = { [key: string]: unknown };
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type Querybuildertypesv5RawRowDTOData =
|
||||
Querybuildertypesv5RawRowDTODataAnyOf | null;
|
||||
export type Querybuildertypesv5RawRowDTOData = Querybuildertypesv5RawRowDTODataAnyOf | null;
|
||||
|
||||
export interface Querybuildertypesv5RawRowDTO {
|
||||
/**
|
||||
@@ -10250,8 +10399,7 @@ export type Querybuildertypesv5QueryRangePreviewResponseDTOCompositeQueryAnyOf =
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type Querybuildertypesv5QueryRangePreviewResponseDTOCompositeQuery =
|
||||
Querybuildertypesv5QueryRangePreviewResponseDTOCompositeQueryAnyOf | null;
|
||||
export type Querybuildertypesv5QueryRangePreviewResponseDTOCompositeQuery = Querybuildertypesv5QueryRangePreviewResponseDTOCompositeQueryAnyOf | null;
|
||||
|
||||
/**
|
||||
* Response from the v5 query range preview (dry-run) endpoint. For each query in the composite query, returns the underlying ClickHouse statement(s) it renders to without executing them (one per PromQL metric selector; exactly one for builder/ClickHouse/trace-operator queries), with the optional EXPLAIN ESTIMATE and granule analysis attached per statement when requested.
|
||||
@@ -10651,6 +10799,9 @@ export interface RuletypesCumulativeWindowDTO {
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The kind of evaluation.
|
||||
*/
|
||||
export enum RuletypesEvaluationCumulativeDTOKind {
|
||||
cumulative = 'cumulative',
|
||||
}
|
||||
@@ -10658,11 +10809,18 @@ export interface RuletypesEvaluationCumulativeDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum cumulative
|
||||
* @description The kind of evaluation.
|
||||
*/
|
||||
kind: RuletypesEvaluationCumulativeDTOKind;
|
||||
/**
|
||||
* @description The cumulative window evaluation specification.
|
||||
*/
|
||||
spec: RuletypesCumulativeWindowDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* The kind of evaluation.
|
||||
*/
|
||||
export enum RuletypesEvaluationRollingDTOKind {
|
||||
rolling = 'rolling',
|
||||
}
|
||||
@@ -10681,8 +10839,12 @@ export interface RuletypesEvaluationRollingDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum rolling
|
||||
* @description The kind of evaluation.
|
||||
*/
|
||||
kind: RuletypesEvaluationRollingDTOKind;
|
||||
/**
|
||||
* @description The rolling window evaluation specification.
|
||||
*/
|
||||
spec: RuletypesRollingWindowDTO;
|
||||
}
|
||||
|
||||
@@ -10888,16 +11050,28 @@ export enum RuletypesSeasonalityDTO {
|
||||
daily = 'daily',
|
||||
weekly = 'weekly',
|
||||
}
|
||||
/**
|
||||
* The kind of threshold.
|
||||
*/
|
||||
export enum RuletypesThresholdBasicDTOKind {
|
||||
basic = 'basic',
|
||||
}
|
||||
/**
|
||||
* The basic threshold specification (array of thresholds).
|
||||
*/
|
||||
export type RuletypesThresholdBasicDTOSpec = RuletypesBasicRuleThresholdsDTO | null;
|
||||
|
||||
export interface RuletypesThresholdBasicDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum basic
|
||||
* @description The kind of threshold.
|
||||
*/
|
||||
kind: RuletypesThresholdBasicDTOKind;
|
||||
spec: RuletypesBasicRuleThresholdsDTO | null;
|
||||
/**
|
||||
* @description The basic threshold specification (array of thresholds).
|
||||
*/
|
||||
spec: RuletypesThresholdBasicDTOSpec;
|
||||
}
|
||||
|
||||
export type RuletypesRuleThresholdDataDTO = RuletypesThresholdBasicDTO;
|
||||
@@ -11560,8 +11734,7 @@ export type SpantypesSpanMapperTestSpanDTOAttributesAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesSpanMapperTestSpanDTOAttributes =
|
||||
SpantypesSpanMapperTestSpanDTOAttributesAnyOf | null;
|
||||
export type SpantypesSpanMapperTestSpanDTOAttributes = SpantypesSpanMapperTestSpanDTOAttributesAnyOf | null;
|
||||
|
||||
export type SpantypesSpanMapperTestSpanDTOResourceAnyOf = {
|
||||
[key: string]: unknown;
|
||||
@@ -11570,8 +11743,7 @@ export type SpantypesSpanMapperTestSpanDTOResourceAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesSpanMapperTestSpanDTOResource =
|
||||
SpantypesSpanMapperTestSpanDTOResourceAnyOf | null;
|
||||
export type SpantypesSpanMapperTestSpanDTOResource = SpantypesSpanMapperTestSpanDTOResourceAnyOf | null;
|
||||
|
||||
export interface SpantypesSpanMapperTestSpanDTO {
|
||||
/**
|
||||
@@ -11683,8 +11855,7 @@ export type SpantypesSpanAggregationResultDTOValueAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesSpanAggregationResultDTOValue =
|
||||
SpantypesSpanAggregationResultDTOValueAnyOf | null;
|
||||
export type SpantypesSpanAggregationResultDTOValue = SpantypesSpanAggregationResultDTOValueAnyOf | null;
|
||||
|
||||
export interface SpantypesSpanAggregationResultDTO {
|
||||
aggregation: SpantypesSpanAggregationTypeDTO;
|
||||
@@ -11886,16 +12057,14 @@ export type SpantypesWaterfallSpanDTOAttributesAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesWaterfallSpanDTOAttributes =
|
||||
SpantypesWaterfallSpanDTOAttributesAnyOf | null;
|
||||
export type SpantypesWaterfallSpanDTOAttributes = SpantypesWaterfallSpanDTOAttributesAnyOf | null;
|
||||
|
||||
export type SpantypesWaterfallSpanDTOResourceAnyOf = { [key: string]: string };
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesWaterfallSpanDTOResource =
|
||||
SpantypesWaterfallSpanDTOResourceAnyOf | null;
|
||||
export type SpantypesWaterfallSpanDTOResource = SpantypesWaterfallSpanDTOResourceAnyOf | null;
|
||||
|
||||
export interface SpantypesWaterfallSpanDTO {
|
||||
/**
|
||||
@@ -12335,8 +12504,7 @@ export type TelemetrytypesGettableFieldKeysDTOKeysAnyOf = {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type TelemetrytypesGettableFieldKeysDTOKeys =
|
||||
TelemetrytypesGettableFieldKeysDTOKeysAnyOf | null;
|
||||
export type TelemetrytypesGettableFieldKeysDTOKeys = TelemetrytypesGettableFieldKeysDTOKeysAnyOf | null;
|
||||
|
||||
export interface TelemetrytypesGettableFieldKeysDTO {
|
||||
/**
|
||||
|
||||
@@ -165,17 +165,15 @@ function createBaseSpec(
|
||||
filter: getFilter(queryData),
|
||||
groupBy:
|
||||
queryData.groupBy?.length > 0
|
||||
? queryData.groupBy.map(
|
||||
(item: any): GroupByKey => ({
|
||||
name: item.key,
|
||||
fieldDataType: item?.dataType || '',
|
||||
fieldContext: item?.type || '',
|
||||
description: item?.description,
|
||||
unit: item?.unit,
|
||||
signal: item?.signal,
|
||||
materialized: item?.materialized,
|
||||
}),
|
||||
)
|
||||
? queryData.groupBy.map((item: any): GroupByKey => ({
|
||||
name: item.key,
|
||||
fieldDataType: item?.dataType || '',
|
||||
fieldContext: item?.type || '',
|
||||
description: item?.description,
|
||||
unit: item?.unit,
|
||||
signal: item?.signal,
|
||||
materialized: item?.materialized,
|
||||
}))
|
||||
: undefined,
|
||||
limit:
|
||||
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
|
||||
@@ -187,14 +185,12 @@ function createBaseSpec(
|
||||
: undefined,
|
||||
order:
|
||||
queryData.orderBy?.length > 0
|
||||
? queryData.orderBy.map(
|
||||
(order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
}),
|
||||
)
|
||||
? queryData.orderBy.map((order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
}))
|
||||
: undefined,
|
||||
legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
|
||||
having: normalizeHaving(queryData.having),
|
||||
@@ -397,17 +393,15 @@ function createTraceOperatorBaseSpec(
|
||||
stepInterval: stepInterval || undefined,
|
||||
groupBy:
|
||||
groupBy?.length > 0
|
||||
? groupBy.map(
|
||||
(item: any): GroupByKey => ({
|
||||
name: item.key,
|
||||
fieldDataType: item?.dataType,
|
||||
fieldContext: item?.type,
|
||||
description: item?.description,
|
||||
unit: item?.unit,
|
||||
signal: item?.signal,
|
||||
materialized: item?.materialized,
|
||||
}),
|
||||
)
|
||||
? groupBy.map((item: any): GroupByKey => ({
|
||||
name: item.key,
|
||||
fieldDataType: item?.dataType,
|
||||
fieldContext: item?.type,
|
||||
description: item?.description,
|
||||
unit: item?.unit,
|
||||
signal: item?.signal,
|
||||
materialized: item?.materialized,
|
||||
}))
|
||||
: undefined,
|
||||
limit:
|
||||
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
|
||||
@@ -416,28 +410,24 @@ function createTraceOperatorBaseSpec(
|
||||
offset: requestType === 'raw' || requestType === 'trace' ? offset : undefined,
|
||||
order:
|
||||
orderBy?.length > 0
|
||||
? orderBy.map(
|
||||
(order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
}),
|
||||
)
|
||||
? orderBy.map((order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
}))
|
||||
: undefined,
|
||||
legend: isEmpty(legend) ? undefined : legend,
|
||||
having: normalizeHaving(having),
|
||||
selectFields: isEmpty(nonEmptySelectColumns)
|
||||
? undefined
|
||||
: nonEmptySelectColumns?.map(
|
||||
(column: any): TelemetryFieldKey => ({
|
||||
name: column.name ?? column.key,
|
||||
fieldDataType:
|
||||
column?.fieldDataType ?? (column?.dataType as FieldDataType),
|
||||
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
|
||||
signal: column?.signal ?? undefined,
|
||||
}),
|
||||
),
|
||||
: nonEmptySelectColumns?.map((column: any): TelemetryFieldKey => ({
|
||||
name: column.name ?? column.key,
|
||||
fieldDataType:
|
||||
column?.fieldDataType ?? (column?.dataType as FieldDataType),
|
||||
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
|
||||
signal: column?.signal ?? undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -610,14 +600,12 @@ export const prepareQueryRangePayloadV5 = (
|
||||
disabled: formulaData.disabled,
|
||||
limit: formulaData.limit ?? undefined,
|
||||
legend: isEmpty(formulaData.legend) ? undefined : formulaData.legend,
|
||||
order: formulaData.orderBy?.map(
|
||||
(order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
}),
|
||||
),
|
||||
order: formulaData.orderBy?.map((order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
})),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -9,3 +9,8 @@
|
||||
margin-inline-end: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: var(--danger-background);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
@@ -111,14 +111,11 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
||||
|
||||
const lineChartRef = useRef<Chart>();
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
(): ToggleGraphProps => ({
|
||||
toggleGraph(graphIndex: number, isVisible: boolean): void {
|
||||
toggleGraph(graphIndex, isVisible, lineChartRef);
|
||||
},
|
||||
}),
|
||||
);
|
||||
useImperativeHandle(ref, (): ToggleGraphProps => ({
|
||||
toggleGraph(graphIndex: number, isVisible: boolean): void {
|
||||
toggleGraph(graphIndex, isVisible, lineChartRef);
|
||||
},
|
||||
}));
|
||||
|
||||
const getGridColor = useCallback(() => {
|
||||
if (currentTheme === undefined) {
|
||||
|
||||
@@ -175,21 +175,19 @@ function OptionsMenu({
|
||||
<div className="title">FORMAT</div>
|
||||
|
||||
<div className="menu-items">
|
||||
{items.map(
|
||||
(item: any): JSX.Element => (
|
||||
<div
|
||||
className="item"
|
||||
key={item.label}
|
||||
onClick={(): void => handleMenuItemClick(item.key)}
|
||||
>
|
||||
<div className={cx('item-label')}>
|
||||
{item.label}
|
||||
{items.map((item: any): JSX.Element => (
|
||||
<div
|
||||
className="item"
|
||||
key={item.label}
|
||||
onClick={(): void => handleMenuItemClick(item.key)}
|
||||
>
|
||||
<div className={cx('item-label')}>
|
||||
{item.label}
|
||||
|
||||
{selectedItem === item.key && <Check size={12} />}
|
||||
</div>
|
||||
{selectedItem === item.key && <Check size={12} />}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,13 +11,8 @@
|
||||
border-top: 1px solid var(--l1-border);
|
||||
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', sans-serif;
|
||||
|
||||
border-right: none;
|
||||
border-left: none;
|
||||
|
||||
@@ -12,13 +12,8 @@ $max-recents-shown: 5;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', sans-serif;
|
||||
|
||||
.query-where-clause-editor-container {
|
||||
position: relative;
|
||||
@@ -138,8 +133,7 @@ $max-recents-shown: 5;
|
||||
// least N recents visible.
|
||||
@for $i from 1 through $max-recents-shown {
|
||||
&:has(> li:nth-of-type(#{$i}) .cm-completionIcon-recent) {
|
||||
max-height: $dropdown-base-height +
|
||||
$recents-header-height +
|
||||
max-height: $dropdown-base-height + $recents-header-height +
|
||||
($i * $recent-row-height) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,26 +13,22 @@ function LabelColumn({ labels, value }: LabelColumnProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="label-column">
|
||||
{newLabels.map(
|
||||
(label: string): JSX.Element => (
|
||||
<BadgeWithTooltip key={label} label={label} value={value} />
|
||||
),
|
||||
)}
|
||||
{newLabels.map((label: string): JSX.Element => (
|
||||
<BadgeWithTooltip key={label} label={label} value={value} />
|
||||
))}
|
||||
{remainingLabels.length > 0 && (
|
||||
<Popover
|
||||
placement="bottomRight"
|
||||
showArrow={false}
|
||||
content={
|
||||
<div>
|
||||
{labels.map(
|
||||
(label: string): JSX.Element => (
|
||||
<div key={label}>
|
||||
<Badge textTransform="none" variant="solid" color="secondary">
|
||||
{getLabelAndValueContent(label, value && value[label])}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
{labels.map((label: string): JSX.Element => (
|
||||
<div key={label}>
|
||||
<Badge textTransform="none" variant="solid" color="secondary">
|
||||
{getLabelAndValueContent(label, value && value[label])}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
trigger="hover"
|
||||
|
||||
@@ -50,14 +50,11 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
|
||||
const onCreateRef = useRef(onCreate);
|
||||
const onDeleteRef = useRef(onDelete);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
(): ToggleGraphProps => ({
|
||||
toggleGraph(graphIndex: number, isVisible: boolean): void {
|
||||
chartRef.current?.setSeries(graphIndex, { show: isVisible });
|
||||
},
|
||||
}),
|
||||
);
|
||||
useImperativeHandle(ref, (): ToggleGraphProps => ({
|
||||
toggleGraph(graphIndex: number, isVisible: boolean): void {
|
||||
chartRef.current?.setSeries(graphIndex, { show: isVisible });
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
onCreateRef.current = onCreate;
|
||||
|
||||
@@ -511,7 +511,7 @@ export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
|
||||
};
|
||||
|
||||
export enum OperatorConfigKeys {
|
||||
'EXCEPTIONS' = 'EXCEPTIONS',
|
||||
EXCEPTIONS = 'EXCEPTIONS',
|
||||
}
|
||||
|
||||
export const OPERATORS_CONFIG = {
|
||||
|
||||
@@ -13,20 +13,19 @@ jest.mock(
|
||||
'../Explorer/Domains/DomainDetails/components/EndPointsDropDown',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(
|
||||
({ setSelectedEndPointName }): JSX.Element => (
|
||||
default: jest
|
||||
.fn()
|
||||
.mockImplementation(({ setSelectedEndPointName }): JSX.Element => (
|
||||
<div data-testid="endpoints-dropdown-mock">
|
||||
<select
|
||||
data-testid="endpoints-select"
|
||||
onChange={(e): void => setSelectedEndPointName(e.target.value)}
|
||||
role="combobox"
|
||||
>
|
||||
<option value="/api/test">/api/test</option>
|
||||
<option value="/api/new-endpoint">/api/new-endpoint</option>
|
||||
</select>
|
||||
</div>
|
||||
),
|
||||
),
|
||||
)),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { useQuery } from 'react-query';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import getAllChannels from 'api/channels/getAll';
|
||||
import classNames from 'classnames';
|
||||
import { ChartLine } from '@signozhq/icons';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { useChannelOptions } from 'hooks/notificationChannels/useChannelOptions';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import AdvancedOptions from '../EvaluationSettings/AdvancedOptions';
|
||||
@@ -21,14 +17,11 @@ function AlertCondition(): JSX.Element {
|
||||
const { alertType, setAlertType } = useCreateAlertState();
|
||||
|
||||
const {
|
||||
data,
|
||||
data: channels,
|
||||
isLoading: isLoadingChannels,
|
||||
isError: isErrorChannels,
|
||||
refetch: refreshChannels,
|
||||
} = useQuery<SuccessResponseV2<Channels[]>, APIError>(['getChannels'], {
|
||||
queryFn: () => getAllChannels(),
|
||||
});
|
||||
const channels = data?.data || [];
|
||||
} = useChannelOptions();
|
||||
|
||||
const showMultipleTabs =
|
||||
alertType === AlertTypes.ANOMALY_BASED_ALERT ||
|
||||
|
||||
@@ -2,12 +2,12 @@ import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import { ChannelOption } from 'hooks/notificationChannels/useChannelOptions';
|
||||
|
||||
import { CreateAlertProvider } from '../../context';
|
||||
import AlertThreshold from '../AlertThreshold';
|
||||
|
||||
const mockChannels: Channels[] = [];
|
||||
const mockChannels: ChannelOption[] = [];
|
||||
const mockRefreshChannels = jest.fn();
|
||||
const mockIsLoadingChannels = false;
|
||||
const mockIsErrorChannels = false;
|
||||
@@ -85,16 +85,17 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
}));
|
||||
|
||||
// Mock getAllChannels API
|
||||
jest.mock('api/channels/getAll', () => ({
|
||||
jest.mock('hooks/notificationChannels/useChannelOptions', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: [
|
||||
{ id: '1', name: 'Email Channel' },
|
||||
{ id: '2', name: 'Slack Channel' },
|
||||
] as Channels[],
|
||||
}),
|
||||
),
|
||||
useChannelOptions: jest.fn(() => ({
|
||||
data: [
|
||||
{ id: '1', name: 'Email Channel' },
|
||||
{ id: '2', name: 'Slack Channel' },
|
||||
] as ChannelOption[],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
refetch: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock alert format categories
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { DefaultOptionType } from 'antd/es/select';
|
||||
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
|
||||
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
|
||||
import * as appHooks from 'providers/App/App';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import { ChannelOption } from 'hooks/notificationChannels/useChannelOptions';
|
||||
|
||||
import * as context from '../../context';
|
||||
import ThresholdItem from '../ThresholdItem';
|
||||
@@ -57,7 +57,7 @@ const mockThreshold = {
|
||||
color: '#ff0000',
|
||||
};
|
||||
|
||||
const mockChannels: Channels[] = [
|
||||
const mockChannels: ChannelOption[] = [
|
||||
{
|
||||
id: TEST_CONSTANTS.CHANNEL_1,
|
||||
name: TEST_CONSTANTS.EMAIL_CHANNEL_NAME,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DefaultOptionType } from 'antd/es/select';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import { ChannelOption } from 'hooks/notificationChannels/useChannelOptions';
|
||||
|
||||
import {
|
||||
NotificationSettingsAction,
|
||||
@@ -21,7 +21,7 @@ export interface ThresholdItemProps {
|
||||
updateThreshold: UpdateThreshold;
|
||||
removeThreshold: (thresholdId: string) => void;
|
||||
showRemoveButton: boolean;
|
||||
channels: Channels[];
|
||||
channels: ChannelOption[];
|
||||
isLoadingChannels: boolean;
|
||||
units: DefaultOptionType[];
|
||||
isErrorChannels: boolean;
|
||||
@@ -29,7 +29,7 @@ export interface ThresholdItemProps {
|
||||
}
|
||||
|
||||
export interface AnomalyAndThresholdProps {
|
||||
channels: Channels[];
|
||||
channels: ChannelOption[];
|
||||
isLoadingChannels: boolean;
|
||||
isErrorChannels: boolean;
|
||||
refreshChannels: () => void;
|
||||
|
||||
@@ -16,19 +16,19 @@ export interface IAdvancedOptionItemProps {
|
||||
}
|
||||
|
||||
export enum RollingWindowTimeframes {
|
||||
'LAST_5_MINUTES' = '5m0s',
|
||||
'LAST_10_MINUTES' = '10m0s',
|
||||
'LAST_15_MINUTES' = '15m0s',
|
||||
'LAST_30_MINUTES' = '30m0s',
|
||||
'LAST_1_HOUR' = '1h0m0s',
|
||||
'LAST_2_HOURS' = '2h0m0s',
|
||||
'LAST_4_HOURS' = '4h0m0s',
|
||||
LAST_5_MINUTES = '5m0s',
|
||||
LAST_10_MINUTES = '10m0s',
|
||||
LAST_15_MINUTES = '15m0s',
|
||||
LAST_30_MINUTES = '30m0s',
|
||||
LAST_1_HOUR = '1h0m0s',
|
||||
LAST_2_HOURS = '2h0m0s',
|
||||
LAST_4_HOURS = '4h0m0s',
|
||||
}
|
||||
|
||||
export enum CumulativeWindowTimeframes {
|
||||
'CURRENT_HOUR' = 'currentHour',
|
||||
'CURRENT_DAY' = 'currentDay',
|
||||
'CURRENT_MONTH' = 'currentMonth',
|
||||
CURRENT_HOUR = 'currentHour',
|
||||
CURRENT_DAY = 'currentDay',
|
||||
CURRENT_MONTH = 'currentMonth',
|
||||
}
|
||||
|
||||
export interface IEvaluationWindowPopoverProps {
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Button, Flex, Form, Select, Tooltip } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import getAll from 'api/channels/getAll';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useChannelOptions } from 'hooks/notificationChannels/useChannelOptions';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { AlertDef, Labels } from 'types/api/alerts/def';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
@@ -47,12 +44,13 @@ function BasicInfo({
|
||||
}: BasicInfoProps): JSX.Element {
|
||||
const { t } = useTranslation('alerts');
|
||||
|
||||
const { isLoading, data, error, isError, refetch } = useQuery<
|
||||
SuccessResponseV2<Channels[]>,
|
||||
APIError
|
||||
>(['getChannels'], {
|
||||
queryFn: () => getAll(),
|
||||
});
|
||||
const {
|
||||
isLoading,
|
||||
data: channels,
|
||||
error,
|
||||
isError,
|
||||
refetch,
|
||||
} = useChannelOptions();
|
||||
|
||||
const { user } = useAppContext();
|
||||
const [addNewChannelPermission] = useComponentPermission(
|
||||
@@ -81,7 +79,7 @@ function BasicInfo({
|
||||
});
|
||||
};
|
||||
|
||||
const noChannels = data?.data?.length === 0;
|
||||
const noChannels = channels.length === 0;
|
||||
const handleCreateNewChannels = useCallback(() => {
|
||||
logEvent('Alert: Create notification channel button clicked', {
|
||||
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
||||
@@ -96,7 +94,7 @@ function BasicInfo({
|
||||
if (!isLoading && isNewRule && !hasLoggedEvent.current) {
|
||||
logEvent('Alert: New alert creation page visited', {
|
||||
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
||||
numberOfChannels: data?.data?.length,
|
||||
numberOfChannels: channels.length,
|
||||
});
|
||||
hasLoggedEvent.current = true;
|
||||
}
|
||||
@@ -230,7 +228,7 @@ function BasicInfo({
|
||||
disabled={shouldBroadCastToAllChannels}
|
||||
currentValue={alertDef.preferredChannels}
|
||||
handleCreateNewChannels={handleCreateNewChannels}
|
||||
channels={data?.data || []}
|
||||
channels={channels}
|
||||
isLoading={isLoading}
|
||||
hasError={isError}
|
||||
error={error as APIError}
|
||||
|
||||
@@ -2,10 +2,10 @@ import { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Select, Spin } from 'antd';
|
||||
import { ChannelOption } from 'hooks/notificationChannels/useChannelOptions';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { StyledCreateChannelOption, StyledSelect } from './styles';
|
||||
@@ -16,7 +16,7 @@ export interface ChannelSelectProps {
|
||||
onSelectChannels: (s: string[]) => void;
|
||||
onDropdownOpen: () => void;
|
||||
isLoading: boolean;
|
||||
channels: Channels[];
|
||||
channels: ChannelOption[];
|
||||
hasError: boolean;
|
||||
error: APIError;
|
||||
handleCreateNewChannels: () => void;
|
||||
|
||||
@@ -66,9 +66,9 @@ jest.mock('../MetricsTable', () => ({
|
||||
__esModule: true,
|
||||
MetricsTable: jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(): JSX.Element => <div data-testid="metrics-table">Metrics Table</div>,
|
||||
),
|
||||
.mockImplementation((): JSX.Element => (
|
||||
<div data-testid="metrics-table">Metrics Table</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
const mockUseQuery = jest.fn();
|
||||
|
||||
@@ -111,9 +111,9 @@ const POD_STATUS_CATEGORY_MAP: Record<
|
||||
};
|
||||
|
||||
type ErrorStatusKey = {
|
||||
[K in keyof InframonitoringtypesPodCountsByStatusDTO]: (typeof POD_STATUS_CATEGORY_MAP)[K] extends 'error'
|
||||
? K
|
||||
: never;
|
||||
[
|
||||
K in keyof InframonitoringtypesPodCountsByStatusDTO
|
||||
]: (typeof POD_STATUS_CATEGORY_MAP)[K] extends 'error' ? K : never;
|
||||
}[keyof InframonitoringtypesPodCountsByStatusDTO];
|
||||
|
||||
const ERROR_STATUS_LABELS: Record<ErrorStatusKey, string> = {
|
||||
|
||||
@@ -74,12 +74,7 @@
|
||||
}
|
||||
|
||||
tr:has(.active-log) {
|
||||
background-color: rgba(
|
||||
78,
|
||||
116,
|
||||
248,
|
||||
0.5
|
||||
) !important; // same as bg-robin-500
|
||||
background-color: rgba(78, 116, 248, 0.5) !important; // same as bg-robin-500
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import * as useHandleExplorerTabChangeHooks from 'hooks/useHandleExplorerTabChange';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import * as timezoneHooks from 'providers/Timezone';
|
||||
import store from 'store';
|
||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
@@ -147,7 +148,9 @@ function renderExplorer(): void {
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<ErrorModalProvider>
|
||||
<Explorer />
|
||||
<PreferenceContextProvider>
|
||||
<Explorer />
|
||||
</PreferenceContextProvider>
|
||||
</ErrorModalProvider>
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
|
||||
@@ -15,16 +15,14 @@ function PromQLQueryContainer(): JSX.Element | null {
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentQuery.promql.map(
|
||||
(q: IPromQLQuery, idx: number): JSX.Element => (
|
||||
<PromQLQueryBuilder
|
||||
key={q.name}
|
||||
deletable={currentQuery.promql.length > 1}
|
||||
queryIndex={idx}
|
||||
queryData={q}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{currentQuery.promql.map((q: IPromQLQuery, idx: number): JSX.Element => (
|
||||
<PromQLQueryBuilder
|
||||
key={q.name}
|
||||
deletable={currentQuery.promql.length > 1}
|
||||
queryIndex={idx}
|
||||
queryData={q}
|
||||
/>
|
||||
))}
|
||||
<QueryButton
|
||||
onClick={addQueryHandler}
|
||||
icon={<Plus size="md" />}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApiRoutingPolicy } from 'api/routingPolicies/getRoutingPolicies';
|
||||
import { IAppContext, IUser } from 'providers/App/types';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import { ChannelOption } from 'hooks/notificationChannels/useChannelOptions';
|
||||
|
||||
import { RoutingPolicy, UseRoutingPoliciesReturn } from '../types';
|
||||
|
||||
@@ -28,21 +28,13 @@ export const MOCK_ROUTING_POLICY_2: RoutingPolicy = {
|
||||
updatedBy: 'user2@signoz.io',
|
||||
};
|
||||
|
||||
export const MOCK_CHANNEL_1: Channels = {
|
||||
export const MOCK_CHANNEL_1: ChannelOption = {
|
||||
name: 'Channel 1',
|
||||
created_at: '2021-01-01',
|
||||
data: 'data 1',
|
||||
id: '1',
|
||||
type: 'type 1',
|
||||
updated_at: '2021-01-01',
|
||||
};
|
||||
export const MOCK_CHANNEL_2: Channels = {
|
||||
export const MOCK_CHANNEL_2: ChannelOption = {
|
||||
name: 'Channel 2',
|
||||
created_at: '2021-01-02',
|
||||
data: 'data 2',
|
||||
id: '2',
|
||||
type: 'type 2',
|
||||
updated_at: '2021-01-02',
|
||||
};
|
||||
|
||||
export function getUseRoutingPoliciesMockData(
|
||||
|
||||
@@ -77,12 +77,14 @@ jest.mock('hooks/routingPolicies/useDeleteRoutingPolicy', () => ({
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
jest.mock('api/channels/getAll', () => ({
|
||||
jest.mock('hooks/notificationChannels/useChannelOptions', () => ({
|
||||
__esModule: true,
|
||||
default: (): any =>
|
||||
Promise.resolve({
|
||||
data: [MOCK_CHANNEL_1, MOCK_CHANNEL_2],
|
||||
}),
|
||||
useChannelOptions: (): any => ({
|
||||
data: [MOCK_CHANNEL_1, MOCK_CHANNEL_2],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
refetch: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const ROUTING_POLICY_1_NAME = 'Routing Policy 1';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import { ChannelOption } from 'hooks/notificationChannels/useChannelOptions';
|
||||
|
||||
export interface RoutingPolicy {
|
||||
id: string;
|
||||
@@ -62,7 +62,7 @@ export interface RoutingPolicyDetailsProps {
|
||||
routingPolicy: RoutingPolicy | null;
|
||||
closeModal: () => void;
|
||||
mode: PolicyDetailsModalMode;
|
||||
channels: Channels[];
|
||||
channels: ChannelOption[];
|
||||
isErrorChannels: boolean;
|
||||
isLoadingChannels: boolean;
|
||||
handlePolicyDetailsModalAction: HandlePolicyDetailsModalAction;
|
||||
@@ -86,7 +86,7 @@ export interface UseRoutingPoliciesReturn {
|
||||
isErrorRoutingPolicies: boolean;
|
||||
refetchRoutingPolicies: () => void;
|
||||
// Channels
|
||||
channels: Channels[];
|
||||
channels: ChannelOption[];
|
||||
isLoadingChannels: boolean;
|
||||
isErrorChannels: boolean;
|
||||
refreshChannels: () => void;
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQuery, useQueryClient } from 'react-query';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import getAllChannels from 'api/channels/getAll';
|
||||
import { GetRoutingPoliciesResponse } from 'api/routingPolicies/getRoutingPolicies';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useCreateRoutingPolicy } from 'hooks/routingPolicies/useCreateRoutingPolicy';
|
||||
import { useDeleteRoutingPolicy } from 'hooks/routingPolicies/useDeleteRoutingPolicy';
|
||||
import { useChannelOptions } from 'hooks/notificationChannels/useChannelOptions';
|
||||
import { useGetRoutingPolicies } from 'hooks/routingPolicies/useGetRoutingPolicies';
|
||||
import { useUpdateRoutingPolicy } from 'hooks/routingPolicies/useUpdateRoutingPolicy';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import {
|
||||
PolicyDetailsModalMode,
|
||||
@@ -83,14 +81,11 @@ function useRoutingPolicies(): UseRoutingPoliciesReturn {
|
||||
|
||||
// Channels list
|
||||
const {
|
||||
data,
|
||||
data: channels,
|
||||
isLoading: isLoadingChannels,
|
||||
isError: isErrorChannels,
|
||||
refetch: refetchChannels,
|
||||
} = useQuery<SuccessResponseV2<Channels[]>, APIError>(['getChannels'], {
|
||||
queryFn: () => getAllChannels(),
|
||||
});
|
||||
const channels = data?.data || [];
|
||||
} = useChannelOptions();
|
||||
|
||||
const refreshChannels = (): void => {
|
||||
refetchChannels();
|
||||
|
||||
@@ -62,25 +62,21 @@ export const getAbbreviatedLabel = (label: string): string => {
|
||||
export const showAllDataSetFromApiResponse = (
|
||||
apiResponse: QueryData[],
|
||||
): LegendEntryProps[] =>
|
||||
apiResponse.map(
|
||||
(item): LegendEntryProps => ({
|
||||
label: getLabelName(
|
||||
item.metric || {},
|
||||
item.queryName || '',
|
||||
item.legend || '',
|
||||
),
|
||||
show: true,
|
||||
}),
|
||||
);
|
||||
apiResponse.map((item): LegendEntryProps => ({
|
||||
label: getLabelName(
|
||||
item.metric || {},
|
||||
item.queryName || '',
|
||||
item.legend || '',
|
||||
),
|
||||
show: true,
|
||||
}));
|
||||
|
||||
export const showAllDataSet = (options: uPlot.Options): LegendEntryProps[] =>
|
||||
options.series
|
||||
.map(
|
||||
(item): LegendEntryProps => ({
|
||||
label: item.label || '',
|
||||
show: true,
|
||||
}),
|
||||
)
|
||||
.map((item): LegendEntryProps => ({
|
||||
label: item.label || '',
|
||||
show: true,
|
||||
}))
|
||||
.filter((_, index) => index !== 0);
|
||||
|
||||
export const saveLegendEntriesToLocalStorage = ({
|
||||
@@ -90,12 +86,10 @@ export const saveLegendEntriesToLocalStorage = ({
|
||||
}: SaveLegendEntriesToLocalStoreProps): void => {
|
||||
const newLegendEntry = {
|
||||
name,
|
||||
dataIndex: options.series.map(
|
||||
(item, index): LegendEntryProps => ({
|
||||
label: item.label || '',
|
||||
show: graphVisibilityState[index],
|
||||
}),
|
||||
),
|
||||
dataIndex: options.series.map((item, index): LegendEntryProps => ({
|
||||
label: item.label || '',
|
||||
show: graphVisibilityState[index],
|
||||
})),
|
||||
};
|
||||
|
||||
let existingEntries: { name: string; dataIndex: LegendEntryProps[] }[] = [];
|
||||
|
||||
68
frontend/src/hooks/notificationChannels/useChannelOptions.ts
Normal file
68
frontend/src/hooks/notificationChannels/useChannelOptions.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
import { listNotificationChannels } from 'api/generated/services/channels';
|
||||
import {
|
||||
AlertmanagertypesChannelListOrderDTO,
|
||||
AlertmanagertypesChannelListSortDTO,
|
||||
AlertmanagertypesListedNotificationChannelDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { range } from 'lodash-es';
|
||||
|
||||
const PAGE_SIZE = 100;
|
||||
|
||||
export const CHANNEL_OPTIONS_QUERY_KEY = ['notificationChannelOptions'];
|
||||
|
||||
export interface ChannelOption {
|
||||
id: string;
|
||||
/** The display name, which is what rules and routing policies reference. */
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type UseChannelOptionsResult = Omit<
|
||||
UseQueryResult<ChannelOption[], Error>,
|
||||
'data'
|
||||
> & {
|
||||
data: ChannelOption[];
|
||||
};
|
||||
|
||||
function fetchPage(
|
||||
offset: number,
|
||||
): ReturnType<typeof listNotificationChannels> {
|
||||
return listNotificationChannels({
|
||||
limit: PAGE_SIZE,
|
||||
offset,
|
||||
sort: AlertmanagertypesChannelListSortDTO.name,
|
||||
order: AlertmanagertypesChannelListOrderDTO.asc,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Every channel, for the pickers that let a rule or a policy name one. The list
|
||||
* API is paginated and silently clamps a larger limit, so the first page reports
|
||||
* the total and any remainder is fetched alongside rather than truncating. One
|
||||
* request covers an org with a page's worth of channels or fewer.
|
||||
*/
|
||||
async function fetchAllChannels(): Promise<ChannelOption[]> {
|
||||
const first = await fetchPage(0);
|
||||
const rest = await Promise.all(
|
||||
range(PAGE_SIZE, first.data.total, PAGE_SIZE).map(fetchPage),
|
||||
);
|
||||
|
||||
const channels: AlertmanagertypesListedNotificationChannelDTO[] = [
|
||||
...first.data.channels,
|
||||
...rest.flatMap((page) => page.data.channels),
|
||||
];
|
||||
|
||||
return channels.map((channel) => ({
|
||||
id: channel.id,
|
||||
name: channel.displayName,
|
||||
}));
|
||||
}
|
||||
|
||||
export function useChannelOptions(): UseChannelOptionsResult {
|
||||
const query = useQuery<ChannelOption[], Error>(
|
||||
CHANNEL_OPTIONS_QUERY_KEY,
|
||||
fetchAllChannels,
|
||||
);
|
||||
|
||||
return { ...query, data: query.data ?? [] };
|
||||
}
|
||||
@@ -1,11 +1,3 @@
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function useSavedViewEnabled(): boolean {
|
||||
const [isEnabled] = useState(
|
||||
() => getLocalStorageKey(LOCALSTORAGE.SAVED_VIEW_ENABLED) === 'true',
|
||||
);
|
||||
|
||||
return isEnabled;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -82,10 +82,9 @@ export function applyAuthZ(
|
||||
items: AuthZDropdownItemType[],
|
||||
state: GateState,
|
||||
): DropdownItemType[] {
|
||||
return items.map(
|
||||
(item): DropdownItemType =>
|
||||
item.type === 'group'
|
||||
? { ...item, items: item.items.map((child) => gateRow(child, state)) }
|
||||
: gateRow(item, state),
|
||||
return items.map((item): DropdownItemType =>
|
||||
item.type === 'group'
|
||||
? { ...item, items: item.items.map((child) => gateRow(child, state)) }
|
||||
: gateRow(item, state),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -241,9 +241,9 @@ describe('Tooltip renderTooltipFooter', () => {
|
||||
});
|
||||
|
||||
it('renders content returned by renderTooltipFooter', () => {
|
||||
const renderTooltipFooter = jest.fn(
|
||||
(): JSX.Element => <div data-testid="custom-tooltip-footer">Footer</div>,
|
||||
);
|
||||
const renderTooltipFooter = jest.fn((): JSX.Element => (
|
||||
<div data-testid="custom-tooltip-footer">Footer</div>
|
||||
));
|
||||
|
||||
renderTooltip({ renderTooltipFooter });
|
||||
|
||||
|
||||
@@ -51,3 +51,38 @@ export const opsGeniePriorityDefaultValue =
|
||||
|
||||
export const pagerDutySeverityTextDefaultValue =
|
||||
'{{ (index .Alerts 0).Labels.severity }}';
|
||||
|
||||
export const notificationChannelsV2 = [
|
||||
{
|
||||
id: '3',
|
||||
name: 'dummy-channel',
|
||||
displayName: 'Dummy-Channel',
|
||||
kind: 'slack',
|
||||
createdAt: '2023-08-09T04:45:19.239344617Z',
|
||||
updatedAt: '2024-06-27T11:37:14.841184399Z',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'oncall-pagerduty',
|
||||
displayName: 'Oncall PagerDuty',
|
||||
kind: 'pagerduty',
|
||||
createdAt: '2024-02-03T04:45:19.239344617Z',
|
||||
updatedAt: '2024-06-28T11:37:14.841184399Z',
|
||||
},
|
||||
];
|
||||
|
||||
export const notificationChannelV2 = {
|
||||
id: '3',
|
||||
name: 'dummy-channel',
|
||||
displayName: 'Dummy-Channel',
|
||||
createdAt: '2023-08-09T04:45:19.239344617Z',
|
||||
updatedAt: '2024-06-27T11:37:14.841184399Z',
|
||||
config: {
|
||||
kind: 'slack',
|
||||
spec: {
|
||||
apiUrl: 'https://hooks.slack.com/services/dummy',
|
||||
channel: '#dummy_channel',
|
||||
sendResolved: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,11 @@ import { rest } from 'msw';
|
||||
|
||||
import commonEnTranslation from '../../public/locales/en/common.json';
|
||||
import enTranslation from '../../public/locales/en/translation.json';
|
||||
import { allAlertChannels } from './__mockdata__/alerts';
|
||||
import {
|
||||
allAlertChannels,
|
||||
notificationChannelsV2,
|
||||
notificationChannelV2,
|
||||
} from './__mockdata__/alerts';
|
||||
import { alertRulesFixture } from './__mockdata__/alert_rules';
|
||||
import { triggeredAlertsFixture } from './__mockdata__/triggered_alerts';
|
||||
import { billingSuccessResponse } from './__mockdata__/billing';
|
||||
@@ -205,6 +209,32 @@ export const handlers = [
|
||||
rest.get('http://localhost/api/v1/channels', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ data: allAlertChannels, status: 'success' })),
|
||||
),
|
||||
rest.get('http://localhost/api/v2/notification_channels', (req, res, ctx) => {
|
||||
const query = req.url.searchParams.get('query')?.toLowerCase() ?? '';
|
||||
const kind = req.url.searchParams.get('kind');
|
||||
const channels = notificationChannelsV2.filter(
|
||||
(channel) =>
|
||||
(!query || channel.displayName.toLowerCase().includes(query)) &&
|
||||
(!kind || channel.kind === kind),
|
||||
);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: { channels, total: channels.length },
|
||||
status: 'success',
|
||||
}),
|
||||
);
|
||||
}),
|
||||
rest.get('http://localhost/api/v2/notification_channels/:id', (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ data: notificationChannelV2, status: 'success' }),
|
||||
),
|
||||
),
|
||||
rest.delete(
|
||||
'http://localhost/api/v2/notification_channels/:id',
|
||||
(_, res, ctx) => res(ctx.status(204)),
|
||||
),
|
||||
rest.get('http://localhost/api/v1/alerts', (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import {
|
||||
alertRuleByIdResponse,
|
||||
ALERT_SCHEMAS,
|
||||
channelsResponse,
|
||||
notificationChannelsResponse,
|
||||
CHANNEL_MAX,
|
||||
FIRST_RULE_NAME,
|
||||
type AlertSchema,
|
||||
@@ -148,8 +148,8 @@ export const alertHistoryMocks = defineStoryMocks({
|
||||
),
|
||||
|
||||
rest.get(
|
||||
'http://localhost/api/v1/channels',
|
||||
response.json(() => channelsResponse(CHANNEL_MAX)),
|
||||
'http://localhost/api/v2/notification_channels',
|
||||
response.json(() => notificationChannelsResponse(CHANNEL_MAX)),
|
||||
),
|
||||
],
|
||||
config: () => ({
|
||||
|
||||
@@ -12,7 +12,7 @@ import { defineStoryMocks } from '@/storybook/controls/defineStoryMocks';
|
||||
import {
|
||||
alertRuleByIdResponse,
|
||||
ALERT_SCHEMAS,
|
||||
channelsResponse,
|
||||
notificationChannelsResponse,
|
||||
CHANNEL_MAX,
|
||||
RULE_STATE_CHOICES,
|
||||
SEVERITY_CHOICES,
|
||||
@@ -116,8 +116,8 @@ export const alertOverviewMocks = defineStoryMocks({
|
||||
),
|
||||
|
||||
rest.get(
|
||||
'http://localhost/api/v1/channels',
|
||||
response.json(() => channelsResponse(values.channels)),
|
||||
'http://localhost/api/v2/notification_channels',
|
||||
response.json(() => notificationChannelsResponse(values.channels)),
|
||||
),
|
||||
|
||||
rest.post(
|
||||
|
||||
@@ -10,7 +10,8 @@ import { defineStoryMocks } from '@/storybook/controls/defineStoryMocks';
|
||||
|
||||
import {
|
||||
CHANNEL_MAX,
|
||||
channelsResponse,
|
||||
notificationChannelByRowId,
|
||||
notificationChannelsPage,
|
||||
} from '../../stories/__story_mockdata__/alerts';
|
||||
import { AlertListTabs } from '../../types';
|
||||
|
||||
@@ -27,8 +28,15 @@ export const channelsMocks = defineStoryMocks({
|
||||
},
|
||||
handlers: (values, response) => [
|
||||
rest.get(
|
||||
'http://localhost/api/v1/channels',
|
||||
response.json(() => channelsResponse(values.channels)),
|
||||
'http://localhost/api/v2/notification_channels/:id',
|
||||
response.json((req) => notificationChannelByRowId(String(req.params.id))),
|
||||
),
|
||||
|
||||
rest.get(
|
||||
'http://localhost/api/v2/notification_channels',
|
||||
response.json((req) =>
|
||||
notificationChannelsPage(values.channels, req.url.searchParams),
|
||||
),
|
||||
),
|
||||
],
|
||||
config: () => ({ route: `/alerts?tab=${AlertListTabs.CHANNELS}` }),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { expect, screen, userEvent, waitFor, within } from 'storybook/test';
|
||||
|
||||
import { storyMocks } from '@/storybook/controls/defineStoryMocks';
|
||||
import type { PageStoryArgs } from '@/storybook/runtime/resolveStory';
|
||||
@@ -13,12 +14,14 @@ const pageStory = storyMocks(channelsMocks, { layout: 'app' });
|
||||
|
||||
/**
|
||||
* Notification channels tab: what a rule can notify, one row per channel.
|
||||
* Search, the type filter and paging all run against the API, so each one is
|
||||
* a request rather than a filter over rows already on screen.
|
||||
*
|
||||
* Route: `/alerts?tab=Channels`.
|
||||
*/
|
||||
const meta = {
|
||||
title: 'Pages/Alerts/Channels/List',
|
||||
tags: ['role-gated'],
|
||||
tags: ['play'],
|
||||
component: AlertList,
|
||||
...pageStory,
|
||||
parameters: { ...pageStory.parameters },
|
||||
@@ -28,9 +31,27 @@ export default meta;
|
||||
|
||||
type Story = StoryObj<ChannelsArgs>;
|
||||
|
||||
/** The list settles after its own request, so the first row outlives the default wait. */
|
||||
const UNTIL_LOADED = { timeout: 15_000 };
|
||||
|
||||
/**
|
||||
* A row's permission check only starts once its menu is open, so the items are
|
||||
* disabled for a moment and a click into that gap lands on nothing.
|
||||
*/
|
||||
const clickRowAction = async (name: string): Promise<void> => {
|
||||
const item = await screen.findByRole('menuitem', { name }, UNTIL_LOADED);
|
||||
|
||||
await waitFor(() => {
|
||||
void expect(item).not.toHaveAttribute('aria-disabled', 'true');
|
||||
void expect(item.closest('[data-disabled]')).toBeNull();
|
||||
}, UNTIL_LOADED);
|
||||
|
||||
await userEvent.click(item);
|
||||
};
|
||||
|
||||
/**
|
||||
* Where notifications go: every configured channel with the integration it
|
||||
* sends through.
|
||||
* sends through, when it was made and when it last changed.
|
||||
*/
|
||||
export const Default: Story = {};
|
||||
|
||||
@@ -52,10 +73,122 @@ export const LoadError: Story = {
|
||||
parameters: { allowConsoleErrors: true },
|
||||
};
|
||||
|
||||
/**
|
||||
* A viewer: the Action column and the New Alert Channel button are gone, and
|
||||
* the button explains who to ask.
|
||||
*/
|
||||
export const Viewer: Story = {
|
||||
args: { access: 'viewer' },
|
||||
/** Interaction: typing a name narrows the list to the channel that matches. */
|
||||
export const Search: Story = {
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('sre-email', undefined, UNTIL_LOADED);
|
||||
await userEvent.type(canvas.getByTestId('channels-search'), 'slack');
|
||||
|
||||
// the table holds the previous page while the debounced request is in
|
||||
// flight, so the row going away is what says the search landed
|
||||
await waitFor(
|
||||
() => expect(canvas.queryByText('sre-email')).not.toBeInTheDocument(),
|
||||
UNTIL_LOADED,
|
||||
);
|
||||
await canvas.findByText('ops-slack');
|
||||
},
|
||||
};
|
||||
|
||||
/** Interaction: the type filter keeps only the channels of the kind it names. */
|
||||
export const KindFilter: Story = {
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('ops-slack', undefined, UNTIL_LOADED);
|
||||
await userEvent.click(canvas.getByTestId('channels-kind-filter'));
|
||||
await userEvent.click(await screen.findByRole('option', { name: 'Email' }));
|
||||
|
||||
await waitFor(
|
||||
() => expect(canvas.queryByText('ops-slack')).not.toBeInTheDocument(),
|
||||
UNTIL_LOADED,
|
||||
);
|
||||
await canvas.findByText('sre-email');
|
||||
},
|
||||
};
|
||||
|
||||
/** Interaction: a search matching nothing offers to clear itself. */
|
||||
export const NoResults: Story = {
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('ops-slack', undefined, UNTIL_LOADED);
|
||||
await userEvent.type(
|
||||
canvas.getByTestId('channels-search'),
|
||||
'no-such-channel',
|
||||
);
|
||||
|
||||
await canvas.findByText('No matching channels', undefined, UNTIL_LOADED);
|
||||
await userEvent.click(canvas.getByRole('button', { name: 'Clear Search' }));
|
||||
await canvas.findByText('ops-slack', undefined, UNTIL_LOADED);
|
||||
},
|
||||
};
|
||||
|
||||
/** Interaction: a row's actions menu, with open, JSON and delete on it. */
|
||||
export const RowActions: Story = {
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('ops-slack', undefined, UNTIL_LOADED);
|
||||
await userEvent.click(
|
||||
canvas.getByRole('button', { name: 'Actions for ops-slack' }),
|
||||
);
|
||||
|
||||
await screen.findByRole('menu');
|
||||
await screen.findByRole('menuitem', { name: 'View JSON' }, UNTIL_LOADED);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Interaction: the JSON view of a channel, with its secrets masked until the
|
||||
* toolbar is asked to reveal them.
|
||||
*/
|
||||
export const ChannelJson: Story = {
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('ops-slack', undefined, UNTIL_LOADED);
|
||||
await userEvent.click(
|
||||
canvas.getByRole('button', { name: 'Actions for ops-slack' }),
|
||||
);
|
||||
await clickRowAction('View JSON');
|
||||
|
||||
await screen.findByTestId('channel-json-dialog', undefined, UNTIL_LOADED);
|
||||
await screen.findByTestId('channel-json-toggle-secrets');
|
||||
},
|
||||
};
|
||||
|
||||
/** Interaction: deleting a channel asks first, naming what stops reaching it. */
|
||||
export const DeleteConfirm: Story = {
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('ops-slack', undefined, UNTIL_LOADED);
|
||||
await userEvent.click(
|
||||
canvas.getByRole('button', { name: 'Actions for ops-slack' }),
|
||||
);
|
||||
await clickRowAction('Delete');
|
||||
|
||||
// the name sits in its own element, so the sentence spans several nodes
|
||||
const confirmation = await screen.findByRole('dialog');
|
||||
await expect(confirmation.textContent).toContain(
|
||||
'Are you sure you want to delete the ops-slack channel?',
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
/** Interaction: Copy Link confirms it put the channel's URL on the clipboard. */
|
||||
export const CopyLink: Story = {
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('ops-slack', undefined, UNTIL_LOADED);
|
||||
await userEvent.click(
|
||||
canvas.getByRole('button', { name: 'Actions for ops-slack' }),
|
||||
);
|
||||
await clickRowAction('Copy Link');
|
||||
|
||||
await screen.findByText('Link copied to clipboard', undefined, UNTIL_LOADED);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 62px);
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.headerBand {
|
||||
--control-height: 2rem;
|
||||
--combobox-trigger-height: var(--control-height);
|
||||
--select-trigger-height: var(--control-height);
|
||||
--input-height: var(--control-height);
|
||||
--input-wrapper-height: var(--control-height);
|
||||
--button-height: var(--control-height);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-6);
|
||||
flex-shrink: 0;
|
||||
padding: var(--spacing-6) var(--spacing-8);
|
||||
}
|
||||
|
||||
/* Title and count on the left, the create action at the far end, the way the
|
||||
dashboards list opens. */
|
||||
.commandHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.headingBlock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
color: var(--l1-foreground);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 28px;
|
||||
letter-spacing: -0.09px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Search and the type filter, the band's second row. */
|
||||
.filtersRow {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Spans the row, the way the dashboards filter bar does. The input's own
|
||||
wrapper is `width: 100%`, so the basis has to be 0 or `auto` resolves to the
|
||||
full row and wraps the type filter onto the next line. */
|
||||
.searchInput {
|
||||
flex: 1 1 0;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.kindSelect {
|
||||
/* the trigger sizes itself to 100%, so the flex item has to be bounded */
|
||||
flex: 0 0 180px;
|
||||
width: 180px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
/* The options panel is rendered inline (withPortal={false}) so it can be told to
|
||||
match the trigger; Radix publishes that width at the popper layer. */
|
||||
.filtersRow :global([data-slot='select-content']) {
|
||||
width: var(--radix-select-trigger-width);
|
||||
min-width: var(--radix-select-trigger-width);
|
||||
}
|
||||
|
||||
.commandHeader > button {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.searchIcon {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.actionsHeader {
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Left on the table's own defaults, the way Infrastructure Monitoring uses it:
|
||||
transparent cells over the page background rather than a raised slab. */
|
||||
.tableContainer {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
--tanstack-table-row-hover-bg: var(--l2-background-hover);
|
||||
--tanstack-table-row-active-bg: var(--l2-background-active);
|
||||
--tanstack-table-row-height: 42px;
|
||||
|
||||
--tanstack-cell-padding-top-override: 5px;
|
||||
--tanstack-cell-padding-bottom-override: 5px;
|
||||
--tanstack-cell-padding-left-override: 16px;
|
||||
--tanstack-cell-padding-right-override: 16px;
|
||||
|
||||
--tanstack-table-row-odd-bg: color-mix(
|
||||
in srgb,
|
||||
var(--l1-foreground) 2%,
|
||||
transparent
|
||||
);
|
||||
--tanstack-table-row-even-bg: color-mix(
|
||||
in srgb,
|
||||
var(--l1-foreground) 1%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.paginationContainer {
|
||||
padding-right: var(--spacing-12);
|
||||
height: 62px;
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Plus, Search } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { SelectSimple } from '@signozhq/ui/select';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ErrorEmptyState from 'components/Alerts/ErrorEmptyState';
|
||||
import NoResultsEmptyState from 'components/Alerts/NoResultsEmptyState';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { useCalculatedPageSize } from 'components/TanStackTableView/useCalculatedPageSize';
|
||||
import { useTableParams } from 'components/TanStackTableView/useTableParams';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { useUrlSearchState } from 'hooks/useUrlSearchState';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
import ChannelActionsMenu from './components/ChannelActionsMenu/ChannelActionsMenu';
|
||||
import ChannelJsonDialog from './components/ChannelJsonDialog/ChannelJsonDialog';
|
||||
import {
|
||||
KIND_FILTER_ALL,
|
||||
KIND_FILTER_OPTIONS,
|
||||
KIND_KEY,
|
||||
SEARCH_KEY,
|
||||
} from './constants';
|
||||
import { useChannelDelete } from './hooks/useChannelDelete';
|
||||
import { useChannelFormView } from './hooks/useChannelFormView';
|
||||
import { useChannelsData } from './hooks/useChannelsData';
|
||||
import styles from './NotificationChannels.module.scss';
|
||||
import { getChannelColumns } from './table.config';
|
||||
import type { Channel } from './types';
|
||||
|
||||
const QUERY_PARAMS_CONFIG = {
|
||||
orderBy: 'orderBy',
|
||||
page: 'page',
|
||||
limit: 'limit',
|
||||
} as const;
|
||||
|
||||
const DEFAULT_PAGE = 1;
|
||||
const DEFAULT_LIMIT = 10;
|
||||
|
||||
function NotificationChannels(): JSX.Element {
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
const formView = useChannelFormView();
|
||||
|
||||
const { containerRef, calculatedPageSize } = useCalculatedPageSize({
|
||||
rowHeight: 46,
|
||||
});
|
||||
|
||||
const { orderBy, page, limit, setLimit, setPage } = useTableParams(
|
||||
QUERY_PARAMS_CONFIG,
|
||||
{
|
||||
page: DEFAULT_PAGE,
|
||||
limit: DEFAULT_LIMIT,
|
||||
storageKey: 'notification-channels',
|
||||
calculatedPageSize,
|
||||
cleanupOnUnmount: true,
|
||||
},
|
||||
);
|
||||
|
||||
const resetPageOnSearch = useCallback((): void => {
|
||||
setPage(1);
|
||||
}, [setPage]);
|
||||
|
||||
const { searchText, debouncedSearch, handleSearchChange, clearSearch } =
|
||||
useUrlSearchState(SEARCH_KEY, { onDebouncedChange: resetPageOnSearch });
|
||||
|
||||
const [kind, setKind] = useQueryState(
|
||||
KIND_KEY,
|
||||
parseAsString.withDefault(KIND_FILTER_ALL),
|
||||
);
|
||||
|
||||
const handleKindChange = useCallback(
|
||||
(value: string | string[]): void => {
|
||||
const next = value as string;
|
||||
void setKind(next === KIND_FILTER_ALL ? null : next);
|
||||
setPage(1);
|
||||
},
|
||||
[setKind, setPage],
|
||||
);
|
||||
|
||||
const { channels, total, isFetching, isError, refetch } = useChannelsData({
|
||||
search: debouncedSearch,
|
||||
kind,
|
||||
orderBy,
|
||||
page,
|
||||
limit,
|
||||
});
|
||||
|
||||
const deletion = useChannelDelete();
|
||||
const [jsonChannel, setJsonChannel] = useState<Channel | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void logEvent('Alert Channel: Channel list page visited', {});
|
||||
}, []);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(channel: Channel, options?: { newTab?: boolean }): void => {
|
||||
formView.openEdit(channel.id, options);
|
||||
},
|
||||
[formView],
|
||||
);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(channel: Channel): void => formView.openEdit(channel.id),
|
||||
[formView],
|
||||
);
|
||||
|
||||
const handleRowClickNewTab = useCallback(
|
||||
(channel: Channel): void => formView.openEdit(channel.id, { newTab: true }),
|
||||
[formView],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => getChannelColumns(formatTimezoneAdjustedTimestamp),
|
||||
[formatTimezoneAdjustedTimestamp],
|
||||
);
|
||||
|
||||
const columnsWithActions = useMemo(
|
||||
() => [
|
||||
...columns,
|
||||
{
|
||||
id: 'actions',
|
||||
header: (): JSX.Element => (
|
||||
<span className={styles.actionsHeader}>Actions</span>
|
||||
),
|
||||
accessorKey: 'id',
|
||||
width: { fixed: '96px', ignoreLastColumnFill: true },
|
||||
enableSort: false,
|
||||
enableResize: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'right' as const,
|
||||
cell: ({ row }: { row: Channel }): JSX.Element => (
|
||||
<ChannelActionsMenu
|
||||
channel={row}
|
||||
onOpen={handleEdit}
|
||||
onDelete={deletion.request}
|
||||
onViewJson={setJsonChannel}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[columns, handleEdit, deletion.request],
|
||||
);
|
||||
|
||||
const hasActiveFilters = searchText.length > 0 || kind !== KIND_FILTER_ALL;
|
||||
const isEmpty = !isFetching && channels.length === 0;
|
||||
|
||||
const handleClearFilters = useCallback((): void => {
|
||||
clearSearch();
|
||||
void setKind(null);
|
||||
}, [clearSearch, setKind]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<header className={styles.headerBand}>
|
||||
<div className={styles.commandHeader}>
|
||||
<div className={styles.headingBlock}>
|
||||
<Typography.Title className={styles.title}>
|
||||
All notification channels
|
||||
</Typography.Title>
|
||||
<span className={styles.countPill}>{total}</span>
|
||||
</div>
|
||||
<div className={styles.grow} />
|
||||
<TextToolTip
|
||||
text="More details on how to configure notification channels"
|
||||
url="https://signoz.io/docs/setup-alerts-notification/"
|
||||
urlText="Learn More"
|
||||
/>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="md"
|
||||
prefix={<Plus size={14} />}
|
||||
onClick={formView.openCreate}
|
||||
testId="channels-create"
|
||||
>
|
||||
New channel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.filtersRow}>
|
||||
<Input
|
||||
containerClassName={styles.searchInput}
|
||||
placeholder="Search by channel name"
|
||||
value={searchText}
|
||||
onChange={handleSearchChange}
|
||||
suffix={<Search size={14} className={styles.searchIcon} />}
|
||||
testId="channels-search"
|
||||
/>
|
||||
<SelectSimple
|
||||
className={styles.kindSelect}
|
||||
items={KIND_FILTER_OPTIONS}
|
||||
value={kind}
|
||||
onChange={handleKindChange}
|
||||
withPortal={false}
|
||||
testId="channels-kind-filter"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div ref={containerRef} className={styles.tableContainer}>
|
||||
{isError ? (
|
||||
<ErrorEmptyState
|
||||
title="Failed to load notification channels"
|
||||
onRefresh={refetch}
|
||||
/>
|
||||
) : isEmpty && hasActiveFilters ? (
|
||||
<NoResultsEmptyState
|
||||
title="No matching channels"
|
||||
subtitle="No notification channels match your search. Try adjusting your search criteria."
|
||||
onClear={handleClearFilters}
|
||||
clearButtonText="Clear Search"
|
||||
/>
|
||||
) : (
|
||||
<TanStackTable<Channel>
|
||||
data={channels}
|
||||
columns={columnsWithActions}
|
||||
isLoading={isFetching}
|
||||
getRowKey={(row): string => row.id}
|
||||
getItemKey={(row): string => row.id}
|
||||
columnStorageKey="notification-channels-columns"
|
||||
enableQueryParams={QUERY_PARAMS_CONFIG}
|
||||
onRowClick={handleRowClick}
|
||||
onRowClickNewTab={handleRowClickNewTab}
|
||||
pagination={{
|
||||
total,
|
||||
calculatedPageSize,
|
||||
onLimitChange: setLimit,
|
||||
showTotalCount: true,
|
||||
}}
|
||||
paginationClassname={styles.paginationContainer}
|
||||
enableAlternatingRowColors
|
||||
plainTextCellLineClamp={1}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{jsonChannel && (
|
||||
<ChannelJsonDialog
|
||||
channelId={jsonChannel.id}
|
||||
channelName={jsonChannel.displayName}
|
||||
onClose={(): void => setJsonChannel(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deletion.contextHolder}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NotificationChannels;
|
||||
@@ -0,0 +1,98 @@
|
||||
import { server } from 'mocks-server/server';
|
||||
import { screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import { renderChannels } from './renderChannels';
|
||||
|
||||
const FIND = { timeout: 5000 };
|
||||
const TEST_TIMEOUT = 20000;
|
||||
|
||||
describe('Notification channels list', () => {
|
||||
beforeEach(() => {
|
||||
// search, filter and page live in the url, so one test's filter would
|
||||
// otherwise still be applied in the next
|
||||
window.history.replaceState({}, '', '/');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it(
|
||||
'heads the page with the channel count',
|
||||
async () => {
|
||||
renderChannels();
|
||||
|
||||
await expect(
|
||||
screen.findByText('All notification channels', {}, FIND),
|
||||
).resolves.toBeInTheDocument();
|
||||
// the count comes from the API's total, not the loaded page
|
||||
await expect(screen.findByText('2', {}, FIND)).resolves.toBeInTheDocument();
|
||||
},
|
||||
TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
'lists every channel with its type',
|
||||
async () => {
|
||||
renderChannels();
|
||||
|
||||
await expect(
|
||||
screen.findByText('Dummy-Channel', {}, FIND),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(screen.getByText('Oncall PagerDuty')).toBeInTheDocument();
|
||||
// the type reads as plain text, no colour coding
|
||||
expect(screen.getByText('Slack')).toBeInTheDocument();
|
||||
expect(screen.getByText('PagerDuty')).toBeInTheDocument();
|
||||
},
|
||||
TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
'filters server-side as the user types',
|
||||
async () => {
|
||||
renderChannels();
|
||||
|
||||
await screen.findByText('Dummy-Channel', {}, FIND);
|
||||
|
||||
await userEvent.type(screen.getByTestId('channels-search'), 'oncall');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Dummy-Channel')).not.toBeInTheDocument();
|
||||
}, FIND);
|
||||
await expect(
|
||||
screen.findByText('Oncall PagerDuty', {}, FIND),
|
||||
).resolves.toBeInTheDocument();
|
||||
},
|
||||
TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
'asks for confirmation before deleting, from the row menu',
|
||||
async () => {
|
||||
renderChannels();
|
||||
|
||||
await screen.findByText('Dummy-Channel', {}, FIND);
|
||||
// the table paints a loading layer over the rows until the fetch settles
|
||||
const user = userEvent.setup({ delay: null, pointerEventsCheck: 0 });
|
||||
await user.click(screen.getByTestId('channel-actions-3'));
|
||||
// Radix renders the menu into a portal once the trigger is activated
|
||||
await screen.findByRole('menu', {}, FIND);
|
||||
// the menu lives inside the row's clickable cell, so opening it must not
|
||||
// also fire the row click and route to the edit screen
|
||||
expect(window.location.pathname).not.toContain('/channels/edit/');
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('menuitem', { name: 'Delete' }, FIND),
|
||||
);
|
||||
|
||||
// the shared destructive confirmation, the one dashboards also use. The
|
||||
// name sits in its own element, so the sentence spans several nodes.
|
||||
const confirmation = await screen.findByRole('dialog', {}, FIND);
|
||||
expect(confirmation.textContent).toContain(
|
||||
'Are you sure you want to delete the Dummy-Channel channel?',
|
||||
);
|
||||
},
|
||||
TEST_TIMEOUT,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { AlertmanagertypesGettableNotificationChannelDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import {
|
||||
channelToJson,
|
||||
hasSecrets,
|
||||
SECRET_PLACEHOLDER,
|
||||
} from '../utils/channelJson';
|
||||
|
||||
const slackChannel = {
|
||||
id: '1',
|
||||
name: 'prod-alerts',
|
||||
displayName: 'prod alerts',
|
||||
createdAt: '2026-09-01T00:00:00Z',
|
||||
updatedAt: '2026-09-01T00:00:00Z',
|
||||
config: {
|
||||
kind: 'slack',
|
||||
spec: {
|
||||
apiUrl: 'https://hooks.slack.com/services/T/B/SECRET',
|
||||
channel: '#alerts',
|
||||
sendResolved: true,
|
||||
},
|
||||
},
|
||||
} as AlertmanagertypesGettableNotificationChannelDTO;
|
||||
|
||||
describe('channelToJson', () => {
|
||||
it('hides the credential the API returns in the clear', () => {
|
||||
const json = JSON.parse(channelToJson(slackChannel, false));
|
||||
|
||||
expect(json.config.spec.apiUrl).toBe(SECRET_PLACEHOLDER);
|
||||
expect(json.config.spec.channel).toBe('#alerts');
|
||||
expect(channelToJson(slackChannel, false)).not.toContain('SECRET');
|
||||
});
|
||||
|
||||
it('reveals it only when asked', () => {
|
||||
const json = JSON.parse(channelToJson(slackChannel, true));
|
||||
|
||||
expect(json.config.spec.apiUrl).toBe(
|
||||
'https://hooks.slack.com/services/T/B/SECRET',
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves the timestamps and id out, so the JSON can be reused', () => {
|
||||
const json = JSON.parse(channelToJson(slackChannel, true));
|
||||
|
||||
expect(json).not.toHaveProperty('id');
|
||||
expect(json).not.toHaveProperty('createdAt');
|
||||
expect(json.name).toBe('prod-alerts');
|
||||
});
|
||||
|
||||
it('knows which kinds carry a credential', () => {
|
||||
expect(hasSecrets('slack')).toBe(true);
|
||||
expect(hasSecrets('email')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import { render } from 'tests/test-utils';
|
||||
|
||||
import NotificationChannels from '../NotificationChannels';
|
||||
|
||||
/** The list is virtualized, so jsdom needs a viewport for rows to mount. */
|
||||
export function renderChannels(): ReturnType<typeof render> {
|
||||
return render(
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 800, itemHeight: 46 }}>
|
||||
<NotificationChannels />
|
||||
</VirtuosoMockContext.Provider>,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useMemo } from 'react';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import {
|
||||
Braces,
|
||||
Ellipsis,
|
||||
Expand,
|
||||
Link2,
|
||||
SquareArrowOutUpRight,
|
||||
Trash2,
|
||||
} from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Dropdown, type DropdownItemType } from '@signozhq/ui/dropdown';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
|
||||
import type { Channel } from '../../types';
|
||||
import styles from './ChannelActionsMenu.module.scss';
|
||||
|
||||
interface ChannelActionsMenuProps {
|
||||
channel: Channel;
|
||||
onOpen: (channel: Channel, options?: { newTab?: boolean }) => void;
|
||||
onDelete: (channel: Channel) => void;
|
||||
onViewJson: (channel: Channel) => void;
|
||||
}
|
||||
|
||||
function ChannelActionsMenu({
|
||||
channel,
|
||||
onOpen,
|
||||
onDelete,
|
||||
onViewJson,
|
||||
}: ChannelActionsMenuProps): JSX.Element {
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
|
||||
const menuItems = useMemo<DropdownItemType[]>(
|
||||
() => [
|
||||
{
|
||||
type: 'item',
|
||||
value: 'open',
|
||||
label: 'View',
|
||||
prefix: <Expand size={14} />,
|
||||
onClick: (): void => onOpen(channel),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
value: 'open-new-tab',
|
||||
label: 'Open in New Tab',
|
||||
prefix: <SquareArrowOutUpRight size={14} />,
|
||||
onClick: (): void => onOpen(channel, { newTab: true }),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
value: 'copy-link',
|
||||
label: 'Copy Link',
|
||||
prefix: <Link2 size={14} />,
|
||||
onClick: (): void => {
|
||||
setCopy(
|
||||
getAbsoluteUrl(
|
||||
generatePath(ROUTES.CHANNELS_EDIT, { channelId: channel.id }),
|
||||
),
|
||||
);
|
||||
toast.success('Link copied to clipboard');
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
value: 'view-json',
|
||||
label: 'View JSON',
|
||||
prefix: <Braces size={14} />,
|
||||
onClick: (): void => onViewJson(channel),
|
||||
},
|
||||
{ type: 'separator', value: 'before-delete' },
|
||||
{
|
||||
type: 'item',
|
||||
value: 'delete',
|
||||
label: 'Delete',
|
||||
prefix: <Trash2 size={14} />,
|
||||
danger: true,
|
||||
onClick: (): void => onDelete(channel),
|
||||
},
|
||||
],
|
||||
[channel, onOpen, onDelete, onViewJson, setCopy],
|
||||
);
|
||||
|
||||
return (
|
||||
// the row itself opens the channel, so the actions keep their clicks
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className={styles.actions}
|
||||
onClick={(event): void => event.stopPropagation()}
|
||||
>
|
||||
<Dropdown items={menuItems} nativeButton align="end" side="bottom">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon
|
||||
testId={`channel-actions-${channel.id}`}
|
||||
aria-label={`Actions for ${channel.displayName}`}
|
||||
>
|
||||
<Ellipsis size={16} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChannelActionsMenu;
|
||||
@@ -0,0 +1,26 @@
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
height: 100%;
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.editor {
|
||||
flex: 1 1 auto;
|
||||
min-height: 360px;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: var(--radius-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import MEditor from '@monaco-editor/react';
|
||||
import { DrawerWrapper } from '@signozhq/ui/drawer';
|
||||
import { useGetNotificationChannel } from 'api/generated/services/channels';
|
||||
import ErrorEmptyState from 'components/Alerts/ErrorEmptyState';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { READONLY_EDITOR_OPTIONS } from 'container/RolesSettings/monaco.config';
|
||||
import {
|
||||
defineJsonEditorTheme,
|
||||
JSON_EDITOR_THEME,
|
||||
} from 'lib/monaco/jsonEditorTheme';
|
||||
|
||||
import { channelToJson, hasSecrets } from '../../utils/channelJson';
|
||||
import styles from './ChannelJsonDialog.module.scss';
|
||||
import ChannelJsonToolbar from './ChannelJsonToolbar';
|
||||
|
||||
interface ChannelJsonDialogProps {
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function ChannelJsonDialog({
|
||||
channelId,
|
||||
channelName,
|
||||
onClose,
|
||||
}: ChannelJsonDialogProps): JSX.Element {
|
||||
const [copyState, copyToClipboard] = useCopyToClipboard();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [showSecrets, setShowSecrets] = useState(false);
|
||||
|
||||
const { data, isLoading, refetch } = useGetNotificationChannel({
|
||||
id: channelId,
|
||||
});
|
||||
const channel = data?.data;
|
||||
|
||||
const json = useMemo(
|
||||
() => (channel ? channelToJson(channel, showSecrets) : ''),
|
||||
[channel, showSecrets],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (copyState.value) {
|
||||
setCopied(true);
|
||||
const timer = setTimeout(() => setCopied(false), 1500);
|
||||
return (): void => clearTimeout(timer);
|
||||
}
|
||||
return undefined;
|
||||
}, [copyState]);
|
||||
|
||||
const handleCopy = useCallback(
|
||||
(): void => copyToClipboard(json),
|
||||
[copyToClipboard, json],
|
||||
);
|
||||
|
||||
const handleRefetch = useCallback((): void => {
|
||||
void refetch();
|
||||
}, [refetch]);
|
||||
|
||||
const handleToggleSecrets = useCallback(
|
||||
(): void => setShowSecrets((value) => !value),
|
||||
[],
|
||||
);
|
||||
|
||||
const kindHasSecrets = channel ? hasSecrets(channel.config.kind) : false;
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title={`${channelName} / JSON`}
|
||||
width="wide"
|
||||
testId="channel-json-dialog"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Spinner tip="Loading channel..." />
|
||||
) : !channel ? (
|
||||
<ErrorEmptyState title="Failed to load channel" onRefresh={handleRefetch} />
|
||||
) : (
|
||||
<div className={styles.body}>
|
||||
<ChannelJsonToolbar
|
||||
kindHasSecrets={kindHasSecrets}
|
||||
showSecrets={showSecrets}
|
||||
onToggleSecrets={handleToggleSecrets}
|
||||
copied={copied}
|
||||
onCopy={handleCopy}
|
||||
/>
|
||||
|
||||
<div className={styles.editor}>
|
||||
<MEditor
|
||||
value={json}
|
||||
language="json"
|
||||
options={READONLY_EDITOR_OPTIONS}
|
||||
height="100%"
|
||||
theme="vs-dark"
|
||||
// the same theme the dashboard JSON drawer uses: it reads the
|
||||
// page's own tokens, so it follows light and dark
|
||||
onMount={(editor, monaco): void => {
|
||||
defineJsonEditorTheme(monaco, editor.getContainerDomNode());
|
||||
monaco.editor.setTheme(JSON_EDITOR_THEME);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChannelJsonDialog;
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Check, Copy, Eye, EyeOff } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from './ChannelJsonDialog.module.scss';
|
||||
|
||||
interface ChannelJsonToolbarProps {
|
||||
kindHasSecrets: boolean;
|
||||
showSecrets: boolean;
|
||||
onToggleSecrets: () => void;
|
||||
copied: boolean;
|
||||
onCopy: () => void;
|
||||
}
|
||||
|
||||
function ChannelJsonToolbar({
|
||||
kindHasSecrets,
|
||||
showSecrets,
|
||||
onToggleSecrets,
|
||||
copied,
|
||||
onCopy,
|
||||
}: ChannelJsonToolbarProps): JSX.Element {
|
||||
return (
|
||||
<div className={styles.toolbar}>
|
||||
{kindHasSecrets && (
|
||||
<Typography.Text color="muted" size="sm">
|
||||
{showSecrets ? 'Credentials are visible.' : 'Credentials are hidden.'}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<span className={styles.spacer} />
|
||||
{kindHasSecrets && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
prefix={showSecrets ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||
onClick={onToggleSecrets}
|
||||
testId="channel-json-toggle-secrets"
|
||||
>
|
||||
{showSecrets ? 'Hide credentials' : 'Show credentials'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
prefix={
|
||||
copied ? (
|
||||
<Check size={14} color={Color.BG_FOREST_400} />
|
||||
) : (
|
||||
<Copy size={14} />
|
||||
)
|
||||
}
|
||||
onClick={onCopy}
|
||||
testId="channel-json-copy"
|
||||
>
|
||||
{copied ? 'Copied' : 'Copy'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChannelJsonToolbar;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { AlertmanagertypesChannelKindDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export const SEARCH_KEY = 'search';
|
||||
export const KIND_KEY = 'kind';
|
||||
export const PAGE_KEY = 'page';
|
||||
|
||||
export const PAGE_SIZE = 20;
|
||||
export const SEARCH_DEBOUNCE_MS = 300;
|
||||
|
||||
export const CHANNEL_KIND_LABEL: Record<
|
||||
AlertmanagertypesChannelKindDTO,
|
||||
string
|
||||
> = {
|
||||
[AlertmanagertypesChannelKindDTO.slack]: 'Slack',
|
||||
[AlertmanagertypesChannelKindDTO.email]: 'Email',
|
||||
[AlertmanagertypesChannelKindDTO.webhook]: 'Webhook',
|
||||
[AlertmanagertypesChannelKindDTO.pagerduty]: 'PagerDuty',
|
||||
[AlertmanagertypesChannelKindDTO.opsgenie]: 'Opsgenie',
|
||||
[AlertmanagertypesChannelKindDTO.msteams]: 'MS Teams',
|
||||
[AlertmanagertypesChannelKindDTO.googlechat]: 'Google Chat',
|
||||
[AlertmanagertypesChannelKindDTO.jira]: 'Jira',
|
||||
[AlertmanagertypesChannelKindDTO.jsmops]: 'JSM Ops',
|
||||
[AlertmanagertypesChannelKindDTO.incidentio]: 'incident.io',
|
||||
[AlertmanagertypesChannelKindDTO.telegram]: 'Telegram',
|
||||
};
|
||||
|
||||
export const KIND_FILTER_ALL = 'all';
|
||||
|
||||
export const KIND_FILTER_OPTIONS = [
|
||||
{ value: KIND_FILTER_ALL, label: 'All types' },
|
||||
...Object.entries(CHANNEL_KIND_LABEL).map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
})),
|
||||
];
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
invalidateListNotificationChannels,
|
||||
useDeleteNotificationChannel,
|
||||
} from 'api/generated/services/channels';
|
||||
import { useDeleteConfirm } from 'components/DeleteConfirmModal/useDeleteConfirm';
|
||||
import styles from 'components/DeleteConfirmModal/DeleteConfirmModal.module.scss';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { ReactNode } from 'react';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import type { Channel } from '../types';
|
||||
|
||||
export interface ChannelDeleteState {
|
||||
/** Must be rendered by the caller for the confirmation to appear. */
|
||||
contextHolder: ReactNode;
|
||||
request: (channel: Channel) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleting a channel goes through the shared destructive confirmation, the same
|
||||
* one dashboards and saved views use, so the wording and the danger button match.
|
||||
*/
|
||||
export function useChannelDelete(onDeleted?: () => void): ChannelDeleteState {
|
||||
const queryClient = useQueryClient();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const { contextHolder, confirmDelete } = useDeleteConfirm();
|
||||
const { mutateAsync } = useDeleteNotificationChannel();
|
||||
|
||||
const request = (channel: Channel): void => {
|
||||
confirmDelete({
|
||||
title: (
|
||||
<Typography.Title level={5}>
|
||||
Are you sure you want to delete the
|
||||
<Typography.Text className={styles.name}>
|
||||
{' '}
|
||||
{channel.displayName}{' '}
|
||||
</Typography.Text>
|
||||
channel?
|
||||
</Typography.Title>
|
||||
),
|
||||
// Returning the promise keeps Delete loading until the request settles.
|
||||
onConfirm: async (): Promise<void> => {
|
||||
try {
|
||||
await mutateAsync({ pathParams: { id: channel.id } });
|
||||
await invalidateListNotificationChannels(queryClient);
|
||||
onDeleted?.();
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return { contextHolder, request };
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
export interface ChannelFormView {
|
||||
openCreate: () => void;
|
||||
openEdit: (channelId: string, options?: { newTab?: boolean }) => void;
|
||||
}
|
||||
|
||||
/** Create and edit both render on their own route. */
|
||||
export function useChannelFormView(): ChannelFormView {
|
||||
const openEdit = (channelId: string, options?: { newTab?: boolean }): void => {
|
||||
const path = generatePath(ROUTES.CHANNELS_EDIT, { channelId });
|
||||
|
||||
if (options?.newTab) {
|
||||
openInNewTab(path);
|
||||
return;
|
||||
}
|
||||
|
||||
history.push(path);
|
||||
};
|
||||
|
||||
return {
|
||||
openCreate: (): void => history.push(ROUTES.CHANNELS_NEW),
|
||||
openEdit,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useListNotificationChannels } from 'api/generated/services/channels';
|
||||
import {
|
||||
AlertmanagertypesChannelKindDTO,
|
||||
AlertmanagertypesChannelListOrderDTO,
|
||||
AlertmanagertypesChannelListSortDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { SortState } from 'components/TanStackTableView';
|
||||
|
||||
import { KIND_FILTER_ALL } from '../constants';
|
||||
import type { Channel } from '../types';
|
||||
|
||||
/** Table column ids the list API can order by. */
|
||||
const SORT_BY_COLUMN: Record<string, AlertmanagertypesChannelListSortDTO> = {
|
||||
name: AlertmanagertypesChannelListSortDTO.name,
|
||||
createdAt: AlertmanagertypesChannelListSortDTO.created_at,
|
||||
updatedAt: AlertmanagertypesChannelListSortDTO.updated_at,
|
||||
};
|
||||
|
||||
interface UseChannelsDataArgs {
|
||||
search: string;
|
||||
kind: string;
|
||||
orderBy: SortState | null;
|
||||
page: number;
|
||||
limit: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ChannelsData {
|
||||
channels: Channel[];
|
||||
total: number;
|
||||
isFetching: boolean;
|
||||
isError: boolean;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search, kind filter, sort and paging all run on the API, so the table shows
|
||||
* one page at a time rather than filtering a full download in the browser.
|
||||
*/
|
||||
export function useChannelsData({
|
||||
search,
|
||||
kind,
|
||||
orderBy,
|
||||
page,
|
||||
limit,
|
||||
enabled = true,
|
||||
}: UseChannelsDataArgs): ChannelsData {
|
||||
const sort = orderBy ? SORT_BY_COLUMN[orderBy.columnName] : undefined;
|
||||
|
||||
const { data, isFetching, isError, refetch } = useListNotificationChannels(
|
||||
{
|
||||
limit,
|
||||
offset: (page - 1) * limit,
|
||||
...(sort
|
||||
? {
|
||||
sort,
|
||||
order: orderBy?.order as AlertmanagertypesChannelListOrderDTO,
|
||||
}
|
||||
: {}),
|
||||
...(search ? { query: search } : {}),
|
||||
...(kind !== KIND_FILTER_ALL
|
||||
? { kind: kind as AlertmanagertypesChannelKindDTO }
|
||||
: {}),
|
||||
},
|
||||
{ query: { enabled, keepPreviousData: true } },
|
||||
);
|
||||
|
||||
return {
|
||||
channels: data?.data?.channels ?? [],
|
||||
total: data?.data?.total ?? 0,
|
||||
isFetching,
|
||||
isError,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
|
||||
import { CHANNEL_KIND_LABEL } from './constants';
|
||||
import styles from './table.module.scss';
|
||||
import type { Channel } from './types';
|
||||
import { toRelativeTime } from './utils/relativeTime';
|
||||
|
||||
export function getChannelColumns(
|
||||
formatTimezoneAdjustedTimestamp: (date: string, format: string) => string,
|
||||
): TableColumnDef<Channel>[] {
|
||||
const timestampCell = (row: Channel, value: string): JSX.Element => {
|
||||
if (!value) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TanStackTable.HoverTooltip
|
||||
rowId={row.id}
|
||||
title={formatTimezoneAdjustedTimestamp(
|
||||
value,
|
||||
DATE_TIME_FORMATS.DASH_DATETIME,
|
||||
)}
|
||||
>
|
||||
{/* A plain inline-block hugs the text. TanStackTable.Text is a
|
||||
`-webkit-box` for line clamping, which fills the cell and leaves
|
||||
the arrow pointing at the cell's middle instead of the words. */}
|
||||
<span className={styles.timeText}>{toRelativeTime(value)}</span>
|
||||
</TanStackTable.HoverTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'name',
|
||||
header: 'Channel',
|
||||
accessorKey: 'displayName',
|
||||
width: { default: '100%', min: '220px' },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
cell: ({ row }): JSX.Element => (
|
||||
<TanStackTable.Text
|
||||
title={row.displayName}
|
||||
data-testid={`channel-row-${row.id}-name`}
|
||||
>
|
||||
{row.displayName}
|
||||
</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'kind',
|
||||
header: 'Type',
|
||||
accessorKey: 'kind',
|
||||
width: { default: '150px', min: '110px' },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
enableMove: false,
|
||||
cell: ({ row }): JSX.Element => (
|
||||
<TanStackTable.Text data-testid={`channel-row-${row.id}-kind`}>
|
||||
{CHANNEL_KIND_LABEL[row.kind]}
|
||||
</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'createdAt',
|
||||
header: 'Created At',
|
||||
accessorKey: 'createdAt',
|
||||
width: { default: '150px', min: '120px' },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
enableMove: false,
|
||||
cell: ({ row }): JSX.Element => timestampCell(row, row.createdAt),
|
||||
},
|
||||
{
|
||||
id: 'updatedAt',
|
||||
header: 'Updated At',
|
||||
accessorKey: 'updatedAt',
|
||||
width: { default: '150px', min: '120px' },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
enableMove: false,
|
||||
cell: ({ row }): JSX.Element => timestampCell(row, row.updatedAt),
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.timeText {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
font-size: var(--tanstack-plain-cell-font-size, 14px);
|
||||
line-height: var(--tanstack-plain-cell-line-height, 18px);
|
||||
color: inherit;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { AlertmanagertypesListedNotificationChannelDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
/** A channel as the list API returns it: identity and kind, but no config. */
|
||||
export type Channel = AlertmanagertypesListedNotificationChannelDTO;
|
||||
@@ -0,0 +1,50 @@
|
||||
import { AlertmanagertypesGettableNotificationChannelDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export const SECRET_PLACEHOLDER = '••••••••';
|
||||
|
||||
/**
|
||||
* The spec fields the API marks `format: password`. A fetch by ID returns them
|
||||
* in the clear, so the JSON view hides them until asked.
|
||||
*/
|
||||
const SECRET_FIELDS_BY_KIND: Record<string, readonly string[]> = {
|
||||
slack: ['apiUrl'],
|
||||
webhook: ['url', 'password', 'bearerToken'],
|
||||
pagerduty: ['routingKey'],
|
||||
opsgenie: ['apiKey'],
|
||||
msteams: ['webhookUrl'],
|
||||
googlechat: ['webhookUrl'],
|
||||
jira: ['apiToken'],
|
||||
jsmops: ['apiKey'],
|
||||
incidentio: ['token'],
|
||||
};
|
||||
|
||||
export function channelToJson(
|
||||
channel: AlertmanagertypesGettableNotificationChannelDTO,
|
||||
showSecrets: boolean,
|
||||
): string {
|
||||
const { kind, spec } = channel.config;
|
||||
const secrets = SECRET_FIELDS_BY_KIND[kind] ?? [];
|
||||
|
||||
const safeSpec = showSecrets
|
||||
? spec
|
||||
: Object.fromEntries(
|
||||
Object.entries(spec).map(([key, value]) => [
|
||||
key,
|
||||
secrets.includes(key) && value ? SECRET_PLACEHOLDER : value,
|
||||
]),
|
||||
);
|
||||
|
||||
return JSON.stringify(
|
||||
{
|
||||
name: channel.name,
|
||||
displayName: channel.displayName,
|
||||
config: { kind, spec: safeSpec },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
export function hasSecrets(kind: string): boolean {
|
||||
return (SECRET_FIELDS_BY_KIND[kind] ?? []).length > 0;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTimePlugin from 'dayjs/plugin/relativeTime';
|
||||
|
||||
dayjs.extend(relativeTimePlugin);
|
||||
|
||||
/** "2 months ago". The exact timestamp stays available on hover. */
|
||||
export function toRelativeTime(value: string | undefined): string {
|
||||
const parsed = value ? dayjs(value) : null;
|
||||
return parsed?.isValid() ? parsed.fromNow() : '-';
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import {
|
||||
CHANNEL_MAX,
|
||||
channelNames,
|
||||
channelsResponse,
|
||||
notificationChannelsResponse,
|
||||
} from '../../stories/__story_mockdata__/alerts';
|
||||
import { AlertListSubTabs, AlertListTabs } from '../../types';
|
||||
|
||||
@@ -118,10 +118,10 @@ export const routingPoliciesMocks = defineStoryMocks({
|
||||
: res(ctx.status(200), ctx.json({ status: 'success', data: null })),
|
||||
),
|
||||
|
||||
rest.get('http://localhost/api/v1/channels', (_req, res, ctx) =>
|
||||
rest.get('http://localhost/api/v2/notification_channels', (_req, res, ctx) =>
|
||||
values.channelsState === 'error'
|
||||
? res(ctx.status(500), ctx.json({ status: 'error' }))
|
||||
: res(ctx.json(channelsResponse(values.channels))),
|
||||
: res(ctx.json(notificationChannelsResponse(values.channels))),
|
||||
),
|
||||
],
|
||||
config: () => ({
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Tabs, TabsProps } from 'antd';
|
||||
import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import ROUTES from 'constants/routes';
|
||||
import AllAlertChannels from 'container/AllAlertChannels';
|
||||
import AllAlertRules from 'container/ListAlertRules';
|
||||
import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime';
|
||||
import RoutingPolicies from 'container/RoutingPolicies';
|
||||
@@ -16,6 +15,8 @@ import AlertDetails from 'pages/AlertDetails';
|
||||
import ChannelsEdit from 'pages/ChannelsEdit';
|
||||
import ChannelsNew from 'pages/ChannelsNew';
|
||||
|
||||
import NotificationChannels from './NotificationChannels/NotificationChannels';
|
||||
|
||||
import { AlertListSubTabs, AlertListTabs } from './types';
|
||||
|
||||
import './AlertList.styles.scss';
|
||||
@@ -104,7 +105,7 @@ function AllAlertList(): JSX.Element {
|
||||
<div className="alert-rules-container">
|
||||
{isChannelsNew && <ChannelsNew />}
|
||||
{isChannelsEdit && <ChannelsEdit />}
|
||||
{!isChannelDetails && <AllAlertChannels />}
|
||||
{!isChannelDetails && <NotificationChannels />}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -26,6 +26,9 @@ import {
|
||||
type RuletypesAlertCompositeQueryDTO,
|
||||
type RuletypesRuleConditionDTO,
|
||||
type RuletypesRuleDTO,
|
||||
type AlertmanagertypesChannelConfigDTO,
|
||||
type AlertmanagertypesGettableNotificationChannelDTO,
|
||||
type AlertmanagertypesListedNotificationChannelDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { NEW_ALERT_SCHEMA_VERSION } from 'types/api/alerts/alertTypesV2';
|
||||
import type { Channels } from 'types/api/channels/getAll';
|
||||
@@ -704,6 +707,22 @@ const buildChannel = (index: number): Channels => {
|
||||
};
|
||||
};
|
||||
|
||||
const buildListedChannel = (
|
||||
index: number,
|
||||
): AlertmanagertypesListedNotificationChannelDTO => {
|
||||
const seed = CHANNEL_SEEDS[index % CHANNEL_SEEDS.length];
|
||||
|
||||
return {
|
||||
id: String(index + 1),
|
||||
name: seed.name,
|
||||
displayName: seed.name,
|
||||
kind: seed.type as AlertmanagertypesListedNotificationChannelDTO['kind'],
|
||||
createdAt: ago((index + 10) * DAY),
|
||||
updatedAt: ago((index + 1) * DAY),
|
||||
};
|
||||
};
|
||||
|
||||
/** The v2 list, which is what the channel pickers read. */
|
||||
export const channelsResponse = (
|
||||
count: number,
|
||||
): { status: string; data: Channels[] } => ({
|
||||
@@ -711,10 +730,145 @@ export const channelsResponse = (
|
||||
data: Array.from({ length: count }, (_unused, index) => buildChannel(index)),
|
||||
});
|
||||
|
||||
/** The v2 list, which is what the redesigned channels list reads. */
|
||||
export const notificationChannelsResponse = (
|
||||
count: number,
|
||||
): {
|
||||
status: string;
|
||||
data: {
|
||||
channels: AlertmanagertypesListedNotificationChannelDTO[];
|
||||
total: number;
|
||||
};
|
||||
} => {
|
||||
const channels = Array.from({ length: count }, (_unused, index) =>
|
||||
buildListedChannel(index),
|
||||
);
|
||||
|
||||
return { status: 'success', data: { channels, total: channels.length } };
|
||||
};
|
||||
|
||||
/**
|
||||
* The v2 list as the API answers it: search, kind and paging are applied on the
|
||||
* server, so the story has to narrow the rows rather than hand back all of them.
|
||||
*/
|
||||
export const notificationChannelsPage = (
|
||||
count: number,
|
||||
params: URLSearchParams,
|
||||
): {
|
||||
status: string;
|
||||
data: {
|
||||
channels: AlertmanagertypesListedNotificationChannelDTO[];
|
||||
total: number;
|
||||
};
|
||||
} => {
|
||||
const query = params.get('query')?.trim().toLowerCase() ?? '';
|
||||
const kind = params.get('kind') ?? '';
|
||||
const limit = Number(params.get('limit')) || CHANNEL_MAX;
|
||||
const offset = Number(params.get('offset')) || 0;
|
||||
|
||||
const matched = Array.from({ length: count }, (_unused, index) =>
|
||||
buildListedChannel(index),
|
||||
).filter(
|
||||
(channel) =>
|
||||
(!query || channel.displayName.toLowerCase().includes(query)) &&
|
||||
(!kind || channel.kind === kind),
|
||||
);
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
data: {
|
||||
channels: matched.slice(offset, offset + limit),
|
||||
total: matched.length,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* One listed row fetched on its own, as the JSON dialog does. Indexed the same
|
||||
* way the list builds its rows, so a row's id resolves to the kind it displays.
|
||||
*/
|
||||
export const notificationChannelByRowId = (
|
||||
id: string,
|
||||
): ReturnType<typeof notificationChannelResponse> => {
|
||||
const index = (Number(id) - 1 + CHANNEL_SEEDS.length) % CHANNEL_SEEDS.length;
|
||||
|
||||
return notificationChannelResponse(id, CHANNEL_SEEDS[index].type);
|
||||
};
|
||||
|
||||
/** Channel names the alert form and the routing policies pick from. */
|
||||
export const channelNames = (count: number): string[] =>
|
||||
channelsResponse(count).data.map((channel) => channel.name);
|
||||
|
||||
/**
|
||||
* The v2 spec each kind is fetched with, so the edit form renders populated
|
||||
* fields. Keys mirror the generated channel spec schemas.
|
||||
*/
|
||||
const CHANNEL_SPECS: Record<ChannelType, Record<string, unknown>> = {
|
||||
slack: {
|
||||
apiUrl: 'https://hooks.slack.com/services/T000/B000/story-token',
|
||||
channel: '#ops-alerts',
|
||||
title: '[{{ .Status | toUpper }}] {{ .CommonLabels.alertname }}',
|
||||
text: '{{ range .Alerts -}}*Alert:* {{ .Labels.alertname }}\n{{ end }}',
|
||||
sendResolved: true,
|
||||
},
|
||||
webhook: {
|
||||
url: 'https://example.com/story-hook',
|
||||
username: 'story-user',
|
||||
sendResolved: true,
|
||||
},
|
||||
pagerduty: {
|
||||
routingKey: 'story-routing-key',
|
||||
description: '{{ .CommonLabels.alertname }}',
|
||||
severity: 'critical',
|
||||
sendResolved: true,
|
||||
},
|
||||
opsgenie: {
|
||||
apiKey: 'story-opsgenie-key',
|
||||
message: '{{ .CommonLabels.alertname }}',
|
||||
description: 'Story channel',
|
||||
sendResolved: true,
|
||||
},
|
||||
email: {
|
||||
to: 'oncall@story.signoz.io',
|
||||
html: '<p>{{ .CommonLabels.alertname }}</p>',
|
||||
sendResolved: true,
|
||||
},
|
||||
msteams: {
|
||||
webhookUrl: 'https://story.webhook.office.com/webhookb2/story',
|
||||
title: '[{{ .Status | toUpper }}] {{ .CommonLabels.alertname }}',
|
||||
text: 'Story channel',
|
||||
sendResolved: true,
|
||||
},
|
||||
googlechat: {
|
||||
webhookUrl: 'https://chat.googleapis.com/v1/spaces/STORY/messages',
|
||||
title: '[{{ .Status | toUpper }}] {{ .CommonLabels.alertname }}',
|
||||
text: 'Story channel',
|
||||
sendResolved: true,
|
||||
},
|
||||
jira: {
|
||||
site: 'https://story.atlassian.net',
|
||||
project: 'OPS',
|
||||
issueType: 'Task',
|
||||
email: 'story@signoz.io',
|
||||
apiToken: 'story-jira-token',
|
||||
summary: '{{ .CommonLabels.alertname }}',
|
||||
sendResolved: true,
|
||||
},
|
||||
jsmops: {
|
||||
apiKey: 'story-jsm-key',
|
||||
message: '{{ .CommonLabels.alertname }}',
|
||||
description: 'Story channel',
|
||||
sendResolved: true,
|
||||
},
|
||||
incidentio: {
|
||||
url: 'https://api.incident.io/v2/alert_events/http/story',
|
||||
token: 'story-incidentio-token',
|
||||
title: '{{ .CommonLabels.alertname }}',
|
||||
sendResolved: true,
|
||||
},
|
||||
};
|
||||
|
||||
/** A single channel as the v2 API returns it, which the edit screen reads. */
|
||||
export const channelResponse = (
|
||||
id: string,
|
||||
type: ChannelType,
|
||||
@@ -727,6 +881,36 @@ export const channelResponse = (
|
||||
};
|
||||
};
|
||||
|
||||
/** The v2 fetch by id, which is what the redesigned screens read. */
|
||||
export const notificationChannelResponse = (
|
||||
id: string,
|
||||
type: ChannelType,
|
||||
): {
|
||||
status: string;
|
||||
data: AlertmanagertypesGettableNotificationChannelDTO;
|
||||
} => {
|
||||
const index = Math.max(
|
||||
CHANNEL_SEEDS.findIndex((seed) => seed.type === type),
|
||||
0,
|
||||
);
|
||||
const seed = CHANNEL_SEEDS[index];
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
data: {
|
||||
id,
|
||||
name: seed.name,
|
||||
displayName: seed.name,
|
||||
createdAt: ago((index + 10) * DAY),
|
||||
updatedAt: ago((index + 1) * DAY),
|
||||
config: {
|
||||
kind: type,
|
||||
spec: CHANNEL_SPECS[type],
|
||||
} as unknown as AlertmanagertypesChannelConfigDTO,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const CHANNEL_ACTION_OUTCOMES = ['succeeds', 'fails'] as const;
|
||||
|
||||
export type ChannelActionOutcome = (typeof CHANNEL_ACTION_OUTCOMES)[number];
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
alertPreviewSeries,
|
||||
} from '../../AlertList/stories/__story_mockdata__/alertQuery';
|
||||
import {
|
||||
channelsResponse,
|
||||
notificationChannelsResponse,
|
||||
CHANNEL_MAX,
|
||||
} from '../../AlertList/stories/__story_mockdata__/alerts';
|
||||
|
||||
@@ -127,8 +127,8 @@ export const createAlertMocks = defineStoryMocks({
|
||||
),
|
||||
|
||||
rest.get(
|
||||
'http://localhost/api/v1/channels',
|
||||
response.json(() => channelsResponse(values.channels)),
|
||||
'http://localhost/api/v2/notification_channels',
|
||||
response.json(() => notificationChannelsResponse(values.channels)),
|
||||
),
|
||||
|
||||
rest.post(
|
||||
|
||||
@@ -12,7 +12,10 @@ import { DashboardDetailEvents } from 'pages/DashboardPage/constants/events';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
|
||||
import { defineJsonEditorTheme, JSON_EDITOR_THEME } from './editorTheme';
|
||||
import {
|
||||
defineJsonEditorTheme,
|
||||
JSON_EDITOR_THEME,
|
||||
} from 'lib/monaco/jsonEditorTheme';
|
||||
import styles from './JsonEditorDrawer.module.scss';
|
||||
import JsonEditorToolbar from './JsonEditorToolbar';
|
||||
import { useJsonEditor } from './useJsonEditor';
|
||||
|
||||
@@ -39,12 +39,14 @@ export function useContextLinkVariables(): VariableItem[] {
|
||||
return useMemo(
|
||||
() => [
|
||||
...GLOBAL_TIMESTAMP_VARIABLES,
|
||||
...fieldVariableNames.map(
|
||||
(name): VariableItem => ({ name, source: 'Query variable' }),
|
||||
),
|
||||
...dashboardVariableNames.map(
|
||||
(name): VariableItem => ({ name, source: 'Dashboard variable' }),
|
||||
),
|
||||
...fieldVariableNames.map((name): VariableItem => ({
|
||||
name,
|
||||
source: 'Query variable',
|
||||
})),
|
||||
...dashboardVariableNames.map((name): VariableItem => ({
|
||||
name,
|
||||
source: 'Dashboard variable',
|
||||
})),
|
||||
],
|
||||
[fieldVariableNames, dashboardVariableNames],
|
||||
);
|
||||
|
||||
@@ -54,15 +54,13 @@ export function buildDownloadMenuItem({
|
||||
|
||||
const items = DOWNLOAD_FORMAT_OPTIONS.filter(
|
||||
({ format }) => supported[format],
|
||||
).map(
|
||||
({ format, label, icon }): DropdownActionItemType => ({
|
||||
type: 'item',
|
||||
value: `download-${format}`,
|
||||
label,
|
||||
prefix: icon,
|
||||
onClick: (): void => onDownload(format),
|
||||
}),
|
||||
);
|
||||
).map(({ format, label, icon }): DropdownActionItemType => ({
|
||||
type: 'item',
|
||||
value: `download-${format}`,
|
||||
label,
|
||||
prefix: icon,
|
||||
onClick: (): void => onDownload(format),
|
||||
}));
|
||||
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
|
||||
@@ -338,10 +338,12 @@ export const dashboardMocks = defineStoryMocks({
|
||||
|
||||
// The header reads the public link on every load, so it answers even while
|
||||
// the panels are held in the loading or failed state.
|
||||
rest.get('http://localhost/api/v1/dashboards/:id/public', (_req, res, ctx) =>
|
||||
values.published
|
||||
? res(ctx.status(200), ctx.json(publicMeta()))
|
||||
: res(ctx.status(404), ctx.json(NOT_PUBLIC)),
|
||||
rest.get(
|
||||
'http://localhost/api/v1/dashboards/:id/public',
|
||||
(_req, res, ctx) =>
|
||||
values.published
|
||||
? res(ctx.status(200), ctx.json(publicMeta()))
|
||||
: res(ctx.status(404), ctx.json(NOT_PUBLIC)),
|
||||
),
|
||||
|
||||
rest.post('http://localhost/api/v1/dashboards/:id/public', (req, res, ctx) =>
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import {
|
||||
alertRuleByIdResponse,
|
||||
ALERT_SCHEMAS,
|
||||
channelsResponse,
|
||||
notificationChannelsResponse,
|
||||
CHANNEL_MAX,
|
||||
type AlertSchema,
|
||||
} from '../../AlertList/stories/__story_mockdata__/alerts';
|
||||
@@ -77,8 +77,8 @@ export const editRulesMocks = defineStoryMocks({
|
||||
),
|
||||
|
||||
rest.get(
|
||||
'http://localhost/api/v1/channels',
|
||||
response.json(() => channelsResponse(values.channels)),
|
||||
'http://localhost/api/v2/notification_channels',
|
||||
response.json(() => notificationChannelsResponse(values.channels)),
|
||||
),
|
||||
|
||||
rest.post(
|
||||
|
||||
@@ -41,12 +41,10 @@ export const liveLogsQuery = (): Query => {
|
||||
...seed,
|
||||
builder: {
|
||||
...seed.builder,
|
||||
queryData: seed.builder.queryData.map(
|
||||
(data): IBuilderQuery => ({
|
||||
...data,
|
||||
filter: { expression: LIVE_TAIL_FILTER },
|
||||
}),
|
||||
),
|
||||
queryData: seed.builder.queryData.map((data): IBuilderQuery => ({
|
||||
...data,
|
||||
filter: { expression: LIVE_TAIL_FILTER },
|
||||
})),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* AI-owned. Generated and maintained by the `signoz-page-story` skill.
|
||||
* Do not hand-edit: regenerate instead.
|
||||
*/
|
||||
|
||||
import ROUTES from 'constants/routes';
|
||||
import { rest } from 'msw';
|
||||
|
||||
import { countControl } from '@/storybook/controls/controls';
|
||||
import { defineStoryMocks } from '@/storybook/controls/defineStoryMocks';
|
||||
|
||||
import {
|
||||
logsSavedViewsResponse,
|
||||
SAVED_VIEW_MAX,
|
||||
SAVED_VIEWS_PAGE_SIZE,
|
||||
savedViewWriteResponse,
|
||||
} from './__story_mockdata__/savedViews';
|
||||
|
||||
const LIST = 'Views · list';
|
||||
|
||||
export const savedViewsMocks = defineStoryMocks({
|
||||
controls: {
|
||||
views: countControl('Saved views', {
|
||||
group: LIST,
|
||||
description: `The views the org saved for logs. Past ${SAVED_VIEWS_PAGE_SIZE} the table paginates.`,
|
||||
value: 4,
|
||||
max: SAVED_VIEW_MAX,
|
||||
}),
|
||||
},
|
||||
handlers: (values, response) => [
|
||||
rest.get(
|
||||
'http://localhost/api/v1/explorer/views',
|
||||
response.json(() => logsSavedViewsResponse(values.views)),
|
||||
),
|
||||
|
||||
rest.put(
|
||||
'http://localhost/api/v1/explorer/views/:viewId',
|
||||
response.json(savedViewWriteResponse),
|
||||
),
|
||||
|
||||
rest.delete(
|
||||
'http://localhost/api/v1/explorer/views/:viewId',
|
||||
response.json(savedViewWriteResponse),
|
||||
),
|
||||
],
|
||||
config: () => ({ route: ROUTES.LOGS_SAVE_VIEWS }),
|
||||
});
|
||||
@@ -1,88 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { screen, userEvent, within } from 'storybook/test';
|
||||
|
||||
import { storyMocks } from '@/storybook/controls/defineStoryMocks';
|
||||
import type { PageStoryArgs } from '@/storybook/runtime/resolveStory';
|
||||
|
||||
import { savedViewsMocks } from './SavedViews.stories.mocks';
|
||||
|
||||
import LogsModulePage from '../../LogsModulePage';
|
||||
|
||||
type SavedViewsArgs = PageStoryArgs<typeof savedViewsMocks>;
|
||||
|
||||
const pageStory = storyMocks(savedViewsMocks, { layout: 'app' });
|
||||
|
||||
/**
|
||||
* Saved views on the logs explorer, and the actions the legacy editor role gates.
|
||||
*
|
||||
* Route: `/logs/saved-views`.
|
||||
*/
|
||||
const meta = {
|
||||
title: 'Pages/Logs/Saved Views',
|
||||
tags: ['role-gated', 'play'],
|
||||
component: LogsModulePage,
|
||||
...pageStory,
|
||||
parameters: { ...pageStory.parameters },
|
||||
} satisfies Meta<SavedViewsArgs>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<SavedViewsArgs>;
|
||||
|
||||
/** The page fetches before it renders a row, which outlasts the 1s default. */
|
||||
const untilLoaded = { timeout: 15_000 };
|
||||
|
||||
/** Row actions are per-view icons, so a story that opens one picks the first. */
|
||||
const openFirstAction = async (
|
||||
canvasElement: HTMLElement,
|
||||
testId: string,
|
||||
): Promise<void> => {
|
||||
const [action] = await within(canvasElement).findAllByTestId(
|
||||
testId,
|
||||
undefined,
|
||||
untilLoaded,
|
||||
);
|
||||
|
||||
await userEvent.click(action);
|
||||
};
|
||||
|
||||
/**
|
||||
* The views tab: every view the org saved for logs, who saved it and when, with
|
||||
* the colour the view picker shows it under.
|
||||
*/
|
||||
export const Default: Story = {};
|
||||
|
||||
/** A workspace where nobody has saved a view yet. */
|
||||
export const NoViews: Story = {
|
||||
args: { views: 0 },
|
||||
};
|
||||
|
||||
/** Enough views to paginate, which is the only place the second page shows up. */
|
||||
export const Paginated: Story = {
|
||||
args: { views: 8 },
|
||||
};
|
||||
|
||||
/** Renaming a view: its label and the colour the picker shows it under. */
|
||||
export const EditView: Story = {
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
await openFirstAction(canvasElement, 'edit-view');
|
||||
await screen.findByText('Edit view details', undefined, untilLoaded);
|
||||
},
|
||||
};
|
||||
|
||||
/** The confirmation a view has to pass before it is deleted. */
|
||||
export const DeleteView: Story = {
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
await openFirstAction(canvasElement, 'delete-view');
|
||||
await screen.findByText(
|
||||
/are you sure you want to delete/i,
|
||||
undefined,
|
||||
untilLoaded,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
/** A viewer: the views are listed, none of them editable. */
|
||||
export const ViewerAccess: Story = {
|
||||
args: { access: 'viewer' },
|
||||
};
|
||||
@@ -38,12 +38,10 @@ export function getTableData(data: DropRateResponse[]): RowData[] {
|
||||
}
|
||||
|
||||
const tableData: RowData[] =
|
||||
data?.map(
|
||||
(row: DropRateResponse, index: number): RowData => ({
|
||||
...(row.data as any), // todo-sagar
|
||||
key: index,
|
||||
}),
|
||||
) || [];
|
||||
data?.map((row: DropRateResponse, index: number): RowData => ({
|
||||
...(row.data as any), // todo-sagar
|
||||
key: index,
|
||||
})) || [];
|
||||
|
||||
return tableData;
|
||||
}
|
||||
|
||||
@@ -21,14 +21,12 @@ export function getTableDataForProducerLatencyOverview(
|
||||
|
||||
// Merge the data from both tables
|
||||
const mergedTableData: RowData[] =
|
||||
firstTableData.map(
|
||||
(row, index): RowData => ({
|
||||
...row.data,
|
||||
byte_rate:
|
||||
byteRateMap.get(`${row.data.service_name}--${row.data.topic}`) || 0,
|
||||
key: index,
|
||||
}),
|
||||
) || [];
|
||||
firstTableData.map((row, index): RowData => ({
|
||||
...row.data,
|
||||
byte_rate:
|
||||
byteRateMap.get(`${row.data.service_name}--${row.data.topic}`) || 0,
|
||||
key: index,
|
||||
})) || [];
|
||||
|
||||
return mergedTableData;
|
||||
}
|
||||
|
||||
@@ -108,12 +108,10 @@ export function getTableData(
|
||||
}
|
||||
|
||||
const tableData: RowData[] =
|
||||
data?.result?.[0]?.table?.rows?.map(
|
||||
(row, index: number): RowData => ({
|
||||
...row.data,
|
||||
key: index,
|
||||
}),
|
||||
) || [];
|
||||
data?.result?.[0]?.table?.rows?.map((row, index: number): RowData => ({
|
||||
...row.data,
|
||||
key: index,
|
||||
})) || [];
|
||||
|
||||
return tableData;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
metricTemporalityOf,
|
||||
metricTypeOf,
|
||||
metricsSavedViewsResponse,
|
||||
metricsSavedViewsV2Response,
|
||||
metricsStatsResponse,
|
||||
metricsTreemapResponse,
|
||||
REDUCTION_RULE_MAX,
|
||||
@@ -517,6 +518,11 @@ export const metricsMocks = defineStoryMocks({
|
||||
response.json(() => metricsSavedViewsResponse(values.savedViews)),
|
||||
),
|
||||
|
||||
rest.get(
|
||||
'http://localhost/api/v2/saved_views',
|
||||
response.json(() => metricsSavedViewsV2Response(values.savedViews)),
|
||||
),
|
||||
|
||||
rest.post(
|
||||
'http://localhost/api/v5/query_range',
|
||||
response.json(async (req) => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
InspectMetrics200,
|
||||
ListMetricReductionRules200,
|
||||
ListMetrics200,
|
||||
ListSavedViews200,
|
||||
MetricreductionruletypesGettableReductionRuleDTO,
|
||||
MetricsexplorertypesStatDTO,
|
||||
MetricsexplorertypesTreemapEntryDTO,
|
||||
@@ -24,7 +25,12 @@ import {
|
||||
MetricreductionruletypesMatchTypeDTO,
|
||||
MetrictypesTemporalityDTO,
|
||||
MetrictypesTypeDTO,
|
||||
Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregationDTOSignal as MetricsSignal,
|
||||
Querybuildertypesv5QueryEnvelopeBuilderDTOType,
|
||||
Querybuildertypesv5RequestTypeDTO,
|
||||
SavedviewtypesPanelTypeDTO,
|
||||
SavedviewtypesSchemaVersionDTO,
|
||||
SavedviewtypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { explorerView } from 'mocks-server/__mockdata__/explorer_views';
|
||||
import type { AllViewsProps, ViewProps } from 'types/api/saveViews/types';
|
||||
@@ -785,6 +791,33 @@ export const metricsSavedViewsResponse = (count: number): AllViewsProps => ({
|
||||
})),
|
||||
});
|
||||
|
||||
export const metricsSavedViewsV2Response = (
|
||||
count: number,
|
||||
): ListSavedViews200 => ({
|
||||
status: 'success',
|
||||
data: VIEW_NAMES.slice(0, count).map((displayName, index) => ({
|
||||
id: `storybook-metrics-view-${index + 1}`,
|
||||
name: `storybook-metrics-view-${index + 1}`,
|
||||
source: SavedviewtypesSourceDTO.metrics,
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO.v2,
|
||||
createdAt: '2026-08-20T09:00:00Z',
|
||||
createdBy: 'ada@signoz.io',
|
||||
updatedAt: '2026-08-20T09:00:00Z',
|
||||
updatedBy: 'ada@signoz.io',
|
||||
spec: {
|
||||
displayName,
|
||||
panelType: SavedviewtypesPanelTypeDTO.graph,
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.time_series,
|
||||
queries: [
|
||||
{
|
||||
type: Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query,
|
||||
spec: { name: 'A', signal: MetricsSignal.metrics },
|
||||
},
|
||||
],
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
const FIELD_KEYS = [
|
||||
'service.name',
|
||||
'deployment.environment',
|
||||
|
||||
@@ -201,30 +201,28 @@ export default function Support(): JSX.Element {
|
||||
</div>
|
||||
|
||||
<div className="support-channels">
|
||||
{supportChannels.map(
|
||||
(channel): JSX.Element => (
|
||||
<Card className="support-channel" key={channel.key}>
|
||||
<div className="support-channel-content">
|
||||
<Title truncate={1} level={5} className="support-channel-title">
|
||||
{channel.icon}
|
||||
{channel.name}{' '}
|
||||
</Title>
|
||||
<Text> {channel.title} </Text>
|
||||
</div>
|
||||
{supportChannels.map((channel): JSX.Element => (
|
||||
<Card className="support-channel" key={channel.key}>
|
||||
<div className="support-channel-content">
|
||||
<Title truncate={1} level={5} className="support-channel-title">
|
||||
{channel.icon}
|
||||
{channel.name}{' '}
|
||||
</Title>
|
||||
<Text> {channel.title} </Text>
|
||||
</div>
|
||||
|
||||
<div className="support-channel-action">
|
||||
<Button
|
||||
className="periscope-btn secondary support-channel-btn"
|
||||
type="default"
|
||||
onClick={(): void => handleChannelClick(channel)}
|
||||
>
|
||||
<Text truncate={1}>{channel.btnText} </Text>
|
||||
{channel.isExternal && <ArrowUpRight size={14} />}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
),
|
||||
)}
|
||||
<div className="support-channel-action">
|
||||
<Button
|
||||
className="periscope-btn secondary support-channel-btn"
|
||||
type="default"
|
||||
onClick={(): void => handleChannelClick(channel)}
|
||||
>
|
||||
<Text truncate={1}>{channel.btnText} </Text>
|
||||
{channel.isExternal && <ArrowUpRight size={14} />}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -144,21 +144,19 @@ const STATUS_CODE_TEMPLATES: SpanTemplate[] = [
|
||||
['503', 'GET /status/unavailable'],
|
||||
['504', 'GET /status/timeout'],
|
||||
['', 'internal.compute'],
|
||||
].map(
|
||||
([statusCode, name]): SpanTemplate => ({
|
||||
service: 'status-api',
|
||||
name,
|
||||
kind: statusCode ? 'Server' : 'Internal',
|
||||
failing: Number(statusCode) >= 500,
|
||||
statusCode,
|
||||
attributes: statusCode
|
||||
? {
|
||||
'http.method': name.split(' ')[0],
|
||||
'http.status_code': Number(statusCode),
|
||||
}
|
||||
: {},
|
||||
}),
|
||||
);
|
||||
].map(([statusCode, name]): SpanTemplate => ({
|
||||
service: 'status-api',
|
||||
name,
|
||||
kind: statusCode ? 'Server' : 'Internal',
|
||||
failing: Number(statusCode) >= 500,
|
||||
statusCode,
|
||||
attributes: statusCode
|
||||
? {
|
||||
'http.method': name.split(' ')[0],
|
||||
'http.status_code': Number(statusCode),
|
||||
}
|
||||
: {},
|
||||
}));
|
||||
|
||||
/**
|
||||
* An agent run: agent, LLM and tool spans, each badged by kind. The LLM spans
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import {
|
||||
act,
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
@@ -316,99 +315,17 @@ describe('TracesExplorer -', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('test for explorer options', async () => {
|
||||
const { getByText, getByTestId } = renderWithTracesExplorerRouter(
|
||||
<TracesExplorer />,
|
||||
['/traces-explorer/?panelType=list&selectedExplorerView=list'],
|
||||
);
|
||||
|
||||
// assert explorer options - action btns
|
||||
[
|
||||
'Save this view',
|
||||
'Create an Alert',
|
||||
'Add to Dashboard',
|
||||
'Select a view',
|
||||
].forEach((val) => expect(getByText(val)).toBeInTheDocument());
|
||||
|
||||
const hideExplorerOption = getByTestId('hide-toolbar');
|
||||
expect(hideExplorerOption).toBeInTheDocument();
|
||||
fireEvent.click(hideExplorerOption);
|
||||
|
||||
// explorer options should hide and show btn should be present
|
||||
await expect(
|
||||
screen.findByTestId('show-explorer-option'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('hide-toolbar')).toBeNull();
|
||||
|
||||
// show explorer options
|
||||
const showExplorerOption = screen.getByTestId('show-explorer-option');
|
||||
expect(showExplorerOption).toBeInTheDocument();
|
||||
fireEvent.click(showExplorerOption);
|
||||
|
||||
// explorer options should show and hide btn should be present
|
||||
await expect(
|
||||
screen.findByTestId('hide-toolbar'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('select a view options - assert and save this view', async () => {
|
||||
const { container } = renderWithTracesExplorerRouter(<TracesExplorer />, [
|
||||
'/traces-explorer/?panelType=list&selectedExplorerView=list',
|
||||
]);
|
||||
|
||||
const viewSearchInput = await waitFor(() => {
|
||||
const el = container.querySelector(
|
||||
'.view-options .ant-select-selection-search-input',
|
||||
) as HTMLElement;
|
||||
expect(el).toBeInTheDocument();
|
||||
return el;
|
||||
});
|
||||
|
||||
fireEvent.mouseDown(viewSearchInput);
|
||||
|
||||
// The saved-views request has to land and re-render the dropdown before the
|
||||
// option exists; findByRole's 1s default is not enough on a loaded runner.
|
||||
await expect(
|
||||
screen.findByRole('option', { name: 'R-test panel' }, { timeout: 5000 }),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
// save this view
|
||||
fireEvent.click(await screen.findByText('Save this view'));
|
||||
|
||||
const saveViewModalInput = await screen.findByPlaceholderText(
|
||||
'e.g. External http method view',
|
||||
);
|
||||
expect(saveViewModalInput).toBeInTheDocument();
|
||||
|
||||
const saveViewModal = await waitFor(() => {
|
||||
const el = document.querySelector('.ant-modal-content') as HTMLElement;
|
||||
expect(el).toBeInTheDocument();
|
||||
return el;
|
||||
});
|
||||
|
||||
await act(async () =>
|
||||
fireEvent.change(saveViewModalInput, { target: { value: 'test view' } }),
|
||||
);
|
||||
|
||||
expect(saveViewModalInput).toHaveValue('test view');
|
||||
await act(async () => {
|
||||
fireEvent.click(within(saveViewModal).getByTestId('save-view-btn'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(successNotification).toHaveBeenCalledWith({
|
||||
message: 'View Saved Successfully',
|
||||
});
|
||||
});
|
||||
}, 15000);
|
||||
|
||||
it('create a dashboard btn assert', async () => {
|
||||
renderWithTracesExplorerRouter(<TracesExplorer />, [
|
||||
'/traces-explorer/?panelType=list&selectedExplorerView=list',
|
||||
]);
|
||||
|
||||
const createDashboardBtn = await screen.findByText('Add to Dashboard');
|
||||
expect(createDashboardBtn).toBeInTheDocument();
|
||||
const createDashboardBtn = await screen.findByTestId(
|
||||
'explorer-add-to-dashboard',
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(createDashboardBtn).not.toHaveAttribute('aria-disabled', 'true'),
|
||||
);
|
||||
fireEvent.click(createDashboardBtn);
|
||||
|
||||
const createDashboardModal = (await screen.findByRole(
|
||||
@@ -431,8 +348,10 @@ describe('TracesExplorer -', () => {
|
||||
'/traces-explorer/?panelType=list&selectedExplorerView=list',
|
||||
]);
|
||||
|
||||
const createAlertBtn = await screen.findByText('Create an Alert');
|
||||
expect(createAlertBtn).toBeInTheDocument();
|
||||
const createAlertBtn = await screen.findByTestId('explorer-create-alert');
|
||||
await waitFor(() =>
|
||||
expect(createAlertBtn).not.toHaveAttribute('aria-disabled', 'true'),
|
||||
);
|
||||
fireEvent.click(createAlertBtn);
|
||||
|
||||
expect(historyPush).toHaveBeenCalledWith(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { screen, userEvent, waitFor } from 'storybook/test';
|
||||
import { screen, userEvent } from 'storybook/test';
|
||||
|
||||
import type { GlobalMockArgs } from '../globals';
|
||||
|
||||
@@ -14,10 +14,10 @@ function EmptyPage(): JSX.Element {
|
||||
const untilLoaded = { timeout: 10_000 };
|
||||
|
||||
/**
|
||||
* The strip along the foot of the shell, behind the `SAVED_VIEW_ENABLED` flag:
|
||||
* what the page puts on the left, the build version when it puts nothing, and
|
||||
* the shell's own Ask Noz and Support on the right. Support follows the chat
|
||||
* support the workspace has, the way the floating bubble it replaces did.
|
||||
* The strip along the foot of the shell: what the page puts on the left, the
|
||||
* build version when it puts nothing, and the shell's own Ask Noz and Support
|
||||
* on the right. Support follows the chat support the workspace has, the way
|
||||
* the floating bubble it replaces did.
|
||||
*
|
||||
* Route: every page inside the app shell; these stories use `/home`.
|
||||
*/
|
||||
@@ -78,38 +78,6 @@ export const Tooltips: Story = {
|
||||
args: { noz: true, support: 'pylon', tooltipsOpen: true },
|
||||
};
|
||||
|
||||
/**
|
||||
* The flag off, on a cloud trial without a card: no strip, and chat support is
|
||||
* the floating bubble in the corner.
|
||||
*/
|
||||
export const WithoutStrip: Story = {
|
||||
args: { bottomStrip: false, noz: true, support: 'add-card' },
|
||||
};
|
||||
|
||||
/** The Add Credit Card modal, opened from the floating bubble with the flag off. */
|
||||
export const WithoutStripAddCardModal: Story = {
|
||||
args: { bottomStrip: false, noz: true, support: 'add-card' },
|
||||
play: async (): Promise<void> => {
|
||||
// The bubble is an icon with no label or test id of its own.
|
||||
const bubble = await waitFor(() => {
|
||||
const found = document.querySelector<HTMLElement>(
|
||||
'.chat-support-gateway-btn',
|
||||
);
|
||||
|
||||
if (!found) {
|
||||
throw new Error('the floating support bubble never rendered');
|
||||
}
|
||||
|
||||
return found;
|
||||
}, untilLoaded);
|
||||
|
||||
await userEvent.click(bubble);
|
||||
await screen.findByRole('dialog', {
|
||||
name: /Add Credit Card for Chat Support/i,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/** Raises a toast and waits for it, so the shot is taken with it on screen. */
|
||||
const raiseToast: NonNullable<Story['play']> = async () => {
|
||||
toast.success('Service account deleted');
|
||||
@@ -117,16 +85,10 @@ const raiseToast: NonNullable<Story['play']> = async () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* A toast with the strip on. Sonner pins itself to the foot of the viewport, so
|
||||
* A toast above the strip. Sonner pins itself to the foot of the viewport, so
|
||||
* without an offset it lands on top of Ask Noz and Support.
|
||||
*/
|
||||
export const WithToast: Story = {
|
||||
args: { noz: true, support: 'pylon' },
|
||||
play: raiseToast,
|
||||
};
|
||||
|
||||
/** The same toast with the flag off, back at sonner's own inset from the viewport. */
|
||||
export const WithoutStripToast: Story = {
|
||||
args: { bottomStrip: false, noz: true, support: 'pylon' },
|
||||
play: raiseToast,
|
||||
};
|
||||
|
||||
@@ -26,14 +26,12 @@ function convertFormulasToV5(
|
||||
limit: formulaData.limit ?? undefined,
|
||||
legend: formulaData.legend,
|
||||
bucketOptions: formulaData.bucketOptions,
|
||||
order: formulaData.orderBy?.map(
|
||||
(order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
}),
|
||||
),
|
||||
order: formulaData.orderBy?.map((order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
})),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/opsgenie"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/pagerduty"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/slack"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/telegram"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/webhook"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/prometheus/alertmanager/config/receiver"
|
||||
@@ -32,6 +33,7 @@ var customNotifierIntegrations = []string{
|
||||
jira.Integration,
|
||||
jsmops.Integration,
|
||||
incidentio.Integration,
|
||||
telegram.Integration,
|
||||
}
|
||||
|
||||
func NewReceiverIntegrations(nc *alertmanagertypes.Receiver, tmpl *template.Template, logger *slog.Logger, templater alertmanagertypes.Templater) ([]notify.Integration, error) {
|
||||
@@ -102,6 +104,11 @@ func NewReceiverIntegrations(nc *alertmanagertypes.Receiver, tmpl *template.Temp
|
||||
return incidentio.New(c, tmpl, l, templater)
|
||||
})
|
||||
}
|
||||
for i, c := range nc.TelegramConfigs {
|
||||
add(telegram.Integration, i, c, func(l *slog.Logger) (notify.Notifier, error) {
|
||||
return telegram.New(c, tmpl, l, templater)
|
||||
})
|
||||
}
|
||||
|
||||
if errs.Len() > 0 {
|
||||
return nil, &errs
|
||||
|
||||
279
pkg/alertmanager/alertmanagernotify/telegram/telegram.go
Normal file
279
pkg/alertmanager/alertmanagernotify/telegram/telegram.go
Normal file
@@ -0,0 +1,279 @@
|
||||
// Copyright (c) 2026 SigNoz, Inc.
|
||||
// Copyright 2022 Prometheus Team
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package telegram delivers alerts to a Telegram chat. It follows upstream's
|
||||
// notifier for everything about the transport and adds the channel title and
|
||||
// the per-rule title and body templates carried on the _title_template and
|
||||
// _body_template annotations.
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/prometheus/alertmanager/config"
|
||||
"github.com/prometheus/alertmanager/notify"
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
commoncfg "github.com/prometheus/common/config"
|
||||
)
|
||||
|
||||
const (
|
||||
Integration = "telegram"
|
||||
|
||||
// Telegram supports 4096 chars max - from https://limits.tginfo.me/en.
|
||||
maxMessageLenRunes = 4096
|
||||
|
||||
parseModeHTML = "HTML"
|
||||
)
|
||||
|
||||
type Notifier struct {
|
||||
conf *alertmanagertypes.TelegramReceiverConfig
|
||||
tmpl *template.Template
|
||||
logger *slog.Logger
|
||||
client *http.Client
|
||||
retrier *notify.Retrier
|
||||
templater alertmanagertypes.Templater
|
||||
}
|
||||
|
||||
func New(conf *alertmanagertypes.TelegramReceiverConfig, t *template.Template, l *slog.Logger, templater alertmanagertypes.Templater, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
|
||||
if conf.HTTPConfig == nil {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "telegram http_config is nil")
|
||||
}
|
||||
|
||||
if conf.APIUrl == nil {
|
||||
withAPIURL := *conf
|
||||
withAPIURL.APIUrl = config.DefaultGlobalConfig().TelegramAPIUrl
|
||||
conf = &withAPIURL
|
||||
}
|
||||
|
||||
// Rendering targets the HTML subset only. A receiver stored through the v1
|
||||
// API can still carry another mode; failing here would fail the whole org's
|
||||
// config, so the send is forced to HTML instead.
|
||||
if conf.ParseMode != parseModeHTML {
|
||||
l.WarnContext(context.Background(), "telegram parse_mode is not supported, sending as HTML", slog.String("parse_mode", conf.ParseMode))
|
||||
htmlConf := *conf
|
||||
htmlConf.ParseMode = parseModeHTML
|
||||
conf = &htmlConf
|
||||
}
|
||||
|
||||
httpclient, err := notify.NewClientWithTracing(*conf.HTTPConfig, Integration, httpOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Notifier{
|
||||
conf: conf,
|
||||
tmpl: t,
|
||||
logger: l,
|
||||
client: httpclient,
|
||||
retrier: ¬ify.Retrier{RetryCodes: []int{http.StatusTooManyRequests}},
|
||||
templater: templater,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
|
||||
key, err := notify.ExtractGroupKey(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
logger := n.logger.With(slog.String("group_key", key.String()))
|
||||
|
||||
messageText, err := n.buildMessage(ctx, logger, as)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
messageText, truncated := notify.TruncateInRunes(messageText, maxMessageLenRunes)
|
||||
if truncated {
|
||||
logger.WarnContext(ctx, "truncated message", slog.Int("max_runes", maxMessageLenRunes))
|
||||
}
|
||||
|
||||
botToken, err := n.getBotToken()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
chatID, err := n.getChatID()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
buf, err := n.encodeSendMessage(chatID, messageText)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
resp, err := notify.PostJSON(ctx, n.client, n.conf.APIUrl.String()+"/bot"+botToken+"/sendMessage", buf) //nolint:bodyclose
|
||||
if err != nil {
|
||||
return true, notify.RedactURL(err)
|
||||
}
|
||||
defer notify.Drain(resp)
|
||||
|
||||
shouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)
|
||||
if err != nil {
|
||||
return shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)
|
||||
}
|
||||
return shouldRetry, err
|
||||
}
|
||||
|
||||
// encodeSendMessage builds the sendMessage payload with every value as a
|
||||
// string, the encoding upstream's telebot client uses.
|
||||
func (n *Notifier) encodeSendMessage(chatID int64, text string) (*bytes.Buffer, error) {
|
||||
params := map[string]string{
|
||||
"chat_id": strconv.FormatInt(chatID, 10),
|
||||
"text": text,
|
||||
"parse_mode": n.conf.ParseMode,
|
||||
"disable_web_page_preview": "true",
|
||||
}
|
||||
if n.conf.DisableNotifications {
|
||||
params["disable_notification"] = "true"
|
||||
}
|
||||
if n.conf.MessageThreadID != 0 {
|
||||
params["message_thread_id"] = strconv.Itoa(n.conf.MessageThreadID)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := json.NewEncoder(&buf).Encode(params); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &buf, nil
|
||||
}
|
||||
|
||||
// buildMessage sends the title in bold above the message. Each half comes from
|
||||
// the rule's template when it has one and from the channel's otherwise; an
|
||||
// empty title sends the message alone.
|
||||
func (n *Notifier) buildMessage(ctx context.Context, logger *slog.Logger, as []*types.Alert) (string, error) {
|
||||
customTitle, customBody := alertmanagertemplate.ExtractTemplatesFromAnnotations(as)
|
||||
|
||||
var result *alertmanagertypes.ExpandResult
|
||||
if customTitle != "" || customBody != "" {
|
||||
var err error
|
||||
result, err = n.templater.Expand(ctx, alertmanagertypes.ExpandRequest{
|
||||
TitleTemplate: customTitle,
|
||||
BodyTemplate: customBody,
|
||||
}, as)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(result.MissingVars) > 0 {
|
||||
logger.WarnContext(ctx, "unresolved variables in rule template", slog.Any("template_missing_vars", result.MissingVars))
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
title, body string
|
||||
err error
|
||||
)
|
||||
if customTitle != "" {
|
||||
title, err = n.renderRuleTemplate(result.Title)
|
||||
} else {
|
||||
title, err = n.renderChannelTemplate(ctx, as, n.conf.Title)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if customBody != "" {
|
||||
body, err = n.renderRuleTemplate(strings.Join(nonEmpty(result.Body), "\n\n"))
|
||||
} else {
|
||||
body, err = n.renderChannelTemplate(ctx, as, n.conf.Message)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
return strings.TrimSpace("<b>" + title + "</b>\n\n" + body), nil
|
||||
}
|
||||
|
||||
// renderChannelTemplate reproduces upstream's rendering, including its choice
|
||||
// of engine under parse_mode HTML: html/template escapes every interpolated
|
||||
// value, so a channel template that does not pipe through html still sends.
|
||||
func (n *Notifier) renderChannelTemplate(ctx context.Context, as []*types.Alert, text string) (string, error) {
|
||||
if text == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var tmplErr error
|
||||
data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
|
||||
|
||||
rendered := notify.TmplHTML(n.tmpl, data, &tmplErr)(text)
|
||||
if tmplErr != nil {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to execute telegram template: %s", tmplErr.Error())
|
||||
}
|
||||
|
||||
return strings.TrimSpace(rendered), nil
|
||||
}
|
||||
|
||||
// renderRuleTemplate converts the templater's output from markdown, which is
|
||||
// the format rule templates are authored in for every channel, into Telegram's
|
||||
// HTML subset. The renderer escapes as it goes, so a stray < or & in an alert
|
||||
// value cannot fail the send with "can't parse entities".
|
||||
func (n *Notifier) renderRuleTemplate(markdown string) (string, error) {
|
||||
rendered, err := markdownrenderer.RenderTelegramHTML(markdown)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return strings.TrimSpace(rendered), nil
|
||||
}
|
||||
|
||||
func (n *Notifier) getBotToken() (string, error) {
|
||||
if n.conf.BotTokenFile == "" {
|
||||
return string(n.conf.BotToken), nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(n.conf.BotTokenFile)
|
||||
if err != nil {
|
||||
return "", errors.WrapInternalf(err, errors.CodeInternal, "could not read %s", n.conf.BotTokenFile)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(content)), nil
|
||||
}
|
||||
|
||||
func (n *Notifier) getChatID() (int64, error) {
|
||||
if n.conf.ChatIDFile == "" {
|
||||
return n.conf.ChatID, nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(n.conf.ChatIDFile)
|
||||
if err != nil {
|
||||
return 0, errors.WrapInternalf(err, errors.CodeInternal, "could not read %s", n.conf.ChatIDFile)
|
||||
}
|
||||
|
||||
chatID, err := strconv.ParseInt(strings.TrimSpace(string(content)), 10, 64)
|
||||
if err != nil {
|
||||
return 0, errors.WrapInternalf(err, errors.CodeInternal, "could not parse chat_id from %s", n.conf.ChatIDFile)
|
||||
}
|
||||
|
||||
return chatID, nil
|
||||
}
|
||||
|
||||
func nonEmpty(parts []string) []string {
|
||||
kept := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if part != "" {
|
||||
kept = append(kept, part)
|
||||
}
|
||||
}
|
||||
|
||||
return kept
|
||||
}
|
||||
390
pkg/alertmanager/alertmanagernotify/telegram/telegram_test.go
Normal file
390
pkg/alertmanager/alertmanagernotify/telegram/telegram_test.go
Normal file
@@ -0,0 +1,390 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
test "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/alertmanagernotifytest"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/prometheus/alertmanager/config"
|
||||
"github.com/prometheus/alertmanager/notify"
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
commoncfg "github.com/prometheus/common/config"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// mockBotAPI answers sendMessage the way api.telegram.org does. Every
|
||||
// parameter is posted as a string.
|
||||
type mockBotAPI struct {
|
||||
srv *httptest.Server
|
||||
mu sync.Mutex
|
||||
sent []map[string]string
|
||||
}
|
||||
|
||||
func newMockBotAPI(t *testing.T) *mockBotAPI {
|
||||
t.Helper()
|
||||
|
||||
return newMockBotAPIWithStatus(t, http.StatusOK)
|
||||
}
|
||||
|
||||
func newMockBotAPIWithStatus(t *testing.T, status int) *mockBotAPI {
|
||||
t.Helper()
|
||||
mock := &mockBotAPI{}
|
||||
mock.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var params map[string]string
|
||||
_ = json.NewDecoder(r.Body).Decode(¶ms)
|
||||
|
||||
mock.mu.Lock()
|
||||
mock.sent = append(mock.sent, params)
|
||||
mock.mu.Unlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if status != http.StatusOK {
|
||||
_, _ = w.Write([]byte(`{"ok":false,"error_code":` + strconv.Itoa(status) + `,"description":"failed"}`))
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"ok":true,"result":{"message_id":1,"date":0,"chat":{"id":` + params["chat_id"] + `,"type":"private"}}}`))
|
||||
}))
|
||||
t.Cleanup(mock.srv.Close)
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
func (m *mockBotAPI) only(t *testing.T) map[string]string {
|
||||
t.Helper()
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
require.Len(t, m.sent, 1)
|
||||
|
||||
return m.sent[0]
|
||||
}
|
||||
|
||||
func newNotifier(t *testing.T, mock *mockBotAPI) *Notifier {
|
||||
t.Helper()
|
||||
|
||||
return newNotifierWithTitle(t, mock, alertmanagertypes.DefaultTelegramTitleTemplate)
|
||||
}
|
||||
|
||||
func newNotifierWithTitle(t *testing.T, mock *mockBotAPI, title string) *Notifier {
|
||||
t.Helper()
|
||||
tmpl := test.CreateTmpl(t)
|
||||
apiURL, err := url.Parse(mock.srv.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
logger := slog.New(slog.DiscardHandler)
|
||||
notifier, err := New(&alertmanagertypes.TelegramReceiverConfig{
|
||||
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
||||
APIUrl: &config.URL{URL: apiURL},
|
||||
BotToken: "123456:tok",
|
||||
ChatID: -1001234567890,
|
||||
ParseMode: "HTML",
|
||||
Title: title,
|
||||
Message: alertmanagertypes.DefaultTelegramMessageTemplate,
|
||||
}, tmpl, logger, alertmanagertemplate.New(tmpl, logger))
|
||||
require.NoError(t, err)
|
||||
|
||||
return notifier
|
||||
}
|
||||
|
||||
func newAlert(host string, annotations model.LabelSet) *types.Alert {
|
||||
merged := model.LabelSet{"summary": "p99 is 612ms"}
|
||||
for name, value := range annotations {
|
||||
merged[name] = value
|
||||
}
|
||||
|
||||
return &types.Alert{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{"alertname": "checkout-latency-high", "severity": "warning", "host": model.LabelValue(host)},
|
||||
Annotations: merged,
|
||||
StartsAt: time.Now(),
|
||||
EndsAt: time.Now().Add(time.Hour),
|
||||
GeneratorURL: "http://am/alerts/overview?ruleId=r1",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyBuildsTheMessage(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
untitledChannel bool
|
||||
alerts []*types.Alert
|
||||
wantExact string
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
}{
|
||||
{
|
||||
name: "NoRuleTemplates_UsesChannelTemplate",
|
||||
alerts: []*types.Alert{newAlert("web-3", nil)},
|
||||
wantContains: []string{
|
||||
"<b>[FIRING:1] checkout-latency-high</b>\n\n<b>Alert:</b>",
|
||||
"<b>Summary:</b> p99 is 612ms",
|
||||
`<a href="http://am/alerts/overview?ruleId=r1">View in SigNoz</a>`,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Receivers stored before the channel title existed have none.
|
||||
name: "UntitledChannel_SendsTheMessageAlone",
|
||||
untitledChannel: true,
|
||||
alerts: []*types.Alert{newAlert("web-3", nil)},
|
||||
wantContains: []string{"<b>Summary:</b> p99 is 612ms"},
|
||||
wantNotContains: []string{"[FIRING:1]"},
|
||||
},
|
||||
{
|
||||
name: "TitleAndBody_ReplaceTheChannelTemplate",
|
||||
alerts: []*types.Alert{newAlert("web-3", model.LabelSet{
|
||||
ruletypes.AnnotationTitleTemplate: "Incident on $labels.host",
|
||||
ruletypes.AnnotationBodyTemplate: "$alert.status for $rule.name",
|
||||
})},
|
||||
wantExact: "<b>Incident on web-3</b>\n\nfiring for checkout-latency-high",
|
||||
wantNotContains: []string{"View in SigNoz"},
|
||||
},
|
||||
{
|
||||
// $-refs are template variables, so the braced form is the real
|
||||
// syntax and the only one that reaches conditionals. A bare ref in
|
||||
// text is rewritten into an action before expansion.
|
||||
name: "BracedAndBareRefs_ExpandAlike",
|
||||
untitledChannel: true,
|
||||
alerts: []*types.Alert{newAlert("web-3", model.LabelSet{
|
||||
ruletypes.AnnotationBodyTemplate: "{{ $rule.name }}|$rule.name|{{ if $alert.is_firing }}up{{ end }}",
|
||||
})},
|
||||
wantExact: "checkout-latency-high|checkout-latency-high|up",
|
||||
},
|
||||
{
|
||||
name: "BodyOnly_KeepsTheChannelTitle",
|
||||
alerts: []*types.Alert{newAlert("web-3", model.LabelSet{
|
||||
ruletypes.AnnotationBodyTemplate: "$alert.status for $rule.name",
|
||||
})},
|
||||
wantExact: "<b>[FIRING:1] checkout-latency-high</b>\n\nfiring for checkout-latency-high",
|
||||
},
|
||||
{
|
||||
name: "BodyOnly_UntitledChannel_OmitsTheTitleLine",
|
||||
untitledChannel: true,
|
||||
alerts: []*types.Alert{newAlert("web-3", model.LabelSet{
|
||||
ruletypes.AnnotationBodyTemplate: "$alert.status for $rule.name",
|
||||
})},
|
||||
wantExact: "firing for checkout-latency-high",
|
||||
},
|
||||
{
|
||||
name: "TitleOnly_ReplacesTheChannelTitle",
|
||||
alerts: []*types.Alert{newAlert("web-3", model.LabelSet{
|
||||
ruletypes.AnnotationTitleTemplate: "Incident on $labels.host",
|
||||
})},
|
||||
wantContains: []string{"<b>Incident on web-3</b>\n\n<b>Alert:</b>", "<b>Summary:</b> p99 is 612ms"},
|
||||
wantNotContains: []string{"[FIRING:1]"},
|
||||
},
|
||||
{
|
||||
// Rule templates are authored in markdown for every channel, so
|
||||
// Telegram converts rather than escaping the markers away.
|
||||
name: "RuleTemplateMarkdown_RenderedToTelegramHTML",
|
||||
untitledChannel: true,
|
||||
alerts: []*types.Alert{newAlert("web-3", model.LabelSet{
|
||||
ruletypes.AnnotationBodyTemplate: "**{{ $rule.name }}** on `{{ $labels.host }}` — [open](https://signoz.io/d?a=1&b=2)",
|
||||
})},
|
||||
wantExact: `<b>checkout-latency-high</b> on <code>web-3</code> — <a href="https://signoz.io/d?a=1&b=2">open</a>`,
|
||||
},
|
||||
{
|
||||
// The templater renders plain text, so anything interpolated from
|
||||
// an alert has to be escaped before a parse_mode HTML send.
|
||||
name: "RuleTemplateOutput_EscapedForHTML",
|
||||
alerts: []*types.Alert{newAlert("web-3", model.LabelSet{
|
||||
"summary": "p99 < 2s & rising",
|
||||
ruletypes.AnnotationTitleTemplate: "$annotations.summary",
|
||||
ruletypes.AnnotationBodyTemplate: "$annotations.summary",
|
||||
})},
|
||||
wantExact: "<b>p99 < 2s & rising</b>\n\np99 < 2s & rising",
|
||||
},
|
||||
{
|
||||
name: "GroupedAlerts_BodyRenderedPerAlert",
|
||||
untitledChannel: true,
|
||||
alerts: []*types.Alert{
|
||||
newAlert("web-3", model.LabelSet{ruletypes.AnnotationBodyTemplate: "host $labels.host"}),
|
||||
newAlert("web-4", model.LabelSet{ruletypes.AnnotationBodyTemplate: "host $labels.host"}),
|
||||
},
|
||||
wantExact: "host web-3\n\nhost web-4",
|
||||
},
|
||||
{
|
||||
// Picking one rule's template for a mixed group would be arbitrary,
|
||||
// so the channel template is used instead.
|
||||
name: "GroupedAlerts_DisagreeingTemplates_FallBackToChannel",
|
||||
alerts: []*types.Alert{
|
||||
newAlert("web-3", model.LabelSet{ruletypes.AnnotationBodyTemplate: "host $labels.host"}),
|
||||
newAlert("web-4", model.LabelSet{ruletypes.AnnotationBodyTemplate: "something else"}),
|
||||
},
|
||||
wantContains: []string{"<b>Summary:</b> p99 is 612ms"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
mock := newMockBotAPI(t)
|
||||
|
||||
title := alertmanagertypes.DefaultTelegramTitleTemplate
|
||||
if testCase.untitledChannel {
|
||||
title = ""
|
||||
}
|
||||
|
||||
retry, err := newNotifierWithTitle(t, mock, title).Notify(notify.WithGroupKey(context.Background(), "1"), testCase.alerts...)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, retry)
|
||||
|
||||
text := mock.only(t)["text"]
|
||||
if testCase.wantExact != "" {
|
||||
assert.Equal(t, testCase.wantExact, text)
|
||||
}
|
||||
for _, want := range testCase.wantContains {
|
||||
assert.Contains(t, text, want)
|
||||
}
|
||||
for _, notWant := range testCase.wantNotContains {
|
||||
assert.NotContains(t, text, notWant)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A v1-written receiver can carry a mode the v2 spec no longer offers. Nothing
|
||||
// renders to those, so the send goes out as HTML without touching the receiver.
|
||||
func TestNewForcesParseModeHTML(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
parseMode string
|
||||
}{
|
||||
{"HTML", "HTML"},
|
||||
{"MarkdownV2", "MarkdownV2"},
|
||||
{"Markdown", "Markdown"},
|
||||
{"Empty", ""},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
mock := newMockBotAPI(t)
|
||||
apiURL, err := url.Parse(mock.srv.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
tmpl := test.CreateTmpl(t)
|
||||
logger := slog.New(slog.DiscardHandler)
|
||||
conf := &alertmanagertypes.TelegramReceiverConfig{
|
||||
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
||||
APIUrl: &config.URL{URL: apiURL},
|
||||
BotToken: "123456:tok",
|
||||
ChatID: -1001234567890,
|
||||
ParseMode: testCase.parseMode,
|
||||
Message: alertmanagertypes.DefaultTelegramMessageTemplate,
|
||||
}
|
||||
notifier, err := New(conf, tmpl, logger, alertmanagertemplate.New(tmpl, logger))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = notifier.Notify(notify.WithGroupKey(context.Background(), "1"), newAlert("web-3", nil))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "HTML", mock.only(t)["parse_mode"])
|
||||
assert.Equal(t, testCase.parseMode, conf.ParseMode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDefaultsAPIURL(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
apiURL string
|
||||
expected string
|
||||
}{
|
||||
{"Unset_DefaultsToTelegram", "", "https://api.telegram.org"},
|
||||
{"Set_KeepsChannelURL", "http://127.0.0.1:9099", "http://127.0.0.1:9099"},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
conf := &alertmanagertypes.TelegramReceiverConfig{
|
||||
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
||||
BotToken: "123456:tok",
|
||||
ChatID: -1001234567890,
|
||||
ParseMode: "HTML",
|
||||
}
|
||||
if testCase.apiURL != "" {
|
||||
apiURL, err := url.Parse(testCase.apiURL)
|
||||
require.NoError(t, err)
|
||||
conf.APIUrl = &config.URL{URL: apiURL}
|
||||
}
|
||||
|
||||
tmpl := test.CreateTmpl(t)
|
||||
logger := slog.New(slog.DiscardHandler)
|
||||
notifier, err := New(conf, tmpl, logger, alertmanagertemplate.New(tmpl, logger))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, notifier.conf.APIUrl)
|
||||
assert.Equal(t, testCase.expected, notifier.conf.APIUrl.String())
|
||||
if testCase.apiURL == "" {
|
||||
assert.Nil(t, conf.APIUrl, "the receiver is not modified")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifySendsTheConfiguredSendOptions(t *testing.T) {
|
||||
mock := newMockBotAPI(t)
|
||||
|
||||
_, err := newNotifier(t, mock).Notify(notify.WithGroupKey(context.Background(), "1"), newAlert("web-3", nil))
|
||||
require.NoError(t, err)
|
||||
|
||||
sent := mock.only(t)
|
||||
assert.Equal(t, "-1001234567890", sent["chat_id"])
|
||||
assert.Equal(t, "HTML", sent["parse_mode"])
|
||||
}
|
||||
|
||||
func TestNotifyRetriesOnlyRateLimitAndServerErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
status int
|
||||
wantErr bool
|
||||
wantRetry bool
|
||||
}{
|
||||
{"OK_NoRetry", http.StatusOK, false, false},
|
||||
{"TooManyRequests_Retries", http.StatusTooManyRequests, true, true},
|
||||
{"InternalServerError_Retries", http.StatusInternalServerError, true, true},
|
||||
{"BadGateway_Retries", http.StatusBadGateway, true, true},
|
||||
{"BadRequest_DoesNotRetry", http.StatusBadRequest, true, false},
|
||||
{"Unauthorized_DoesNotRetry", http.StatusUnauthorized, true, false},
|
||||
{"Forbidden_DoesNotRetry", http.StatusForbidden, true, false},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
mock := newMockBotAPIWithStatus(t, testCase.status)
|
||||
|
||||
retry, err := newNotifier(t, mock).Notify(notify.WithGroupKey(context.Background(), "1"), newAlert("web-3", nil))
|
||||
if testCase.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, testCase.wantRetry, retry)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyPostsToTheBotSendMessageEndpoint(t *testing.T) {
|
||||
var gotPath string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
_, err := newNotifier(t, &mockBotAPI{srv: srv}).Notify(notify.WithGroupKey(context.Background(), "1"), newAlert("web-3", nil))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "/bot123456:tok/sendMessage", gotPath)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
@@ -253,6 +254,24 @@ func (r *BaseRule) GeneratorURL() string {
|
||||
return r.ExternalURL("alerts/overview", params)
|
||||
}
|
||||
|
||||
// ExpandAnnotations runs expand over the rule's annotations, leaving private
|
||||
// ones as they are. Those carry templates the notifier expands against its own
|
||||
// data model, and the ruler's expander rewrites every $-ref into a label
|
||||
// lookup, which blanks the ones that are not labels.
|
||||
func (r *BaseRule) ExpandAnnotations(expand func(string) string) ruletypes.Labels {
|
||||
annotations := make(ruletypes.Labels, 0, len(r.annotations.Map()))
|
||||
for name, value := range r.annotations.Map() {
|
||||
if alertmanagertypes.IsPrivateAnnotation(name) {
|
||||
annotations = append(annotations, ruletypes.Label{Name: name, Value: value})
|
||||
continue
|
||||
}
|
||||
|
||||
annotations = append(annotations, ruletypes.Label{Name: name, Value: expand(value)})
|
||||
}
|
||||
|
||||
return annotations
|
||||
}
|
||||
|
||||
func (r *BaseRule) ExternalURL(path string, params url.Values) string {
|
||||
u := r.externalURL.JoinPath(path)
|
||||
if len(params) > 0 {
|
||||
|
||||
@@ -833,3 +833,55 @@ func labelsKey(lbls []*qbtypes.Label) string {
|
||||
}
|
||||
return temp.Labels().String()
|
||||
}
|
||||
|
||||
func TestExpandAnnotations(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
want map[string]string
|
||||
}{
|
||||
{
|
||||
name: "PublicAnnotation_Expanded",
|
||||
annotations: map[string]string{"summary": "observed {{$value}}"},
|
||||
want: map[string]string{"summary": "observed 0.35"},
|
||||
},
|
||||
{
|
||||
// The ruler's expander rewrites every $-ref into a label lookup, so
|
||||
// expanding these would blank the paths the notifier resolves.
|
||||
name: "PrivateAnnotation_LeftAlone",
|
||||
annotations: map[string]string{ruletypes.AnnotationTitleTemplate: "{{ $rule.name }} on {{ $labels.host }}"},
|
||||
want: map[string]string{ruletypes.AnnotationTitleTemplate: "{{ $rule.name }} on {{ $labels.host }}"},
|
||||
},
|
||||
{
|
||||
name: "MixedAnnotations_OnlyPublicExpanded",
|
||||
annotations: map[string]string{
|
||||
"summary": "observed {{$value}}",
|
||||
ruletypes.AnnotationBodyTemplate: "$alert.status for $rule.name",
|
||||
},
|
||||
want: map[string]string{
|
||||
"summary": "observed 0.35",
|
||||
ruletypes.AnnotationBodyTemplate: "$alert.status for $rule.name",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
rule := &BaseRule{annotations: ruletypes.FromMap(testCase.annotations)}
|
||||
expand := func(text string) string {
|
||||
tmpl := ruletypes.NewTemplateExpander(
|
||||
context.Background(),
|
||||
"{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}"+text,
|
||||
"__alert_test",
|
||||
ruletypes.AlertTemplateData(map[string]string{}, "0.35", "0.32"),
|
||||
nil,
|
||||
)
|
||||
out, err := tmpl.Expand()
|
||||
require.NoError(t, err)
|
||||
return out
|
||||
}
|
||||
|
||||
assert.Equal(t, testCase.want, rule.ExpandAnnotations(expand).Map())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,10 +211,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
lb.Set(ruletypes.AlertRuleIDLabel, r.ID())
|
||||
lb.Set(ruletypes.RuleSourceLabel, r.GeneratorURL())
|
||||
|
||||
annotations := make(ruletypes.Labels, 0, len(r.annotations.Map()))
|
||||
for name, value := range r.annotations.Map() {
|
||||
annotations = append(annotations, ruletypes.Label{Name: name, Value: expand(value)})
|
||||
}
|
||||
annotations := r.ExpandAnnotations(expand)
|
||||
if result.IsMissing {
|
||||
lb.Set(ruletypes.AlertNameLabel, "[No data] "+r.Name())
|
||||
lb.Set(ruletypes.NoDataLabel, "true")
|
||||
|
||||
@@ -295,10 +295,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
lb.Set(ruletypes.AlertRuleIDLabel, r.ID())
|
||||
lb.Set(ruletypes.RuleSourceLabel, r.GeneratorURL())
|
||||
|
||||
annotations := make(ruletypes.Labels, 0, len(r.annotations.Map()))
|
||||
for name, value := range r.annotations.Map() {
|
||||
annotations = append(annotations, ruletypes.Label{Name: name, Value: expand(value)})
|
||||
}
|
||||
annotations := r.ExpandAnnotations(expand)
|
||||
if smpl.IsMissing {
|
||||
lb.Set(ruletypes.AlertNameLabel, "[No data] "+r.Name())
|
||||
lb.Set(ruletypes.NoDataLabel, "true")
|
||||
|
||||
@@ -130,7 +130,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
}
|
||||
|
||||
return defaultDefName
|
||||
}))
|
||||
}), jsonschema.InterceptProp(keepDescriptionOnReferencedProperty))
|
||||
|
||||
reflector.Spec.WithInfo(*(&openapi3.Info{}).
|
||||
WithTitle("SigNoz").
|
||||
@@ -169,6 +169,27 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
}, nil
|
||||
}
|
||||
|
||||
// keepDescriptionOnReferencedProperty wraps a referenced property schema in
|
||||
// allOf when the field carries a description. OpenAPI 3.0 allows no siblings
|
||||
// next to $ref, so without this the description tag on a field whose type is a
|
||||
// named schema (an enum or a struct) is silently dropped from the spec.
|
||||
func keepDescriptionOnReferencedProperty(params jsonschema.InterceptPropParams) error {
|
||||
if !params.Processed || params.PropertySchema == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
schema := params.PropertySchema
|
||||
if schema.Ref == nil || schema.Description == nil || *schema.Description == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ref := *schema.Ref
|
||||
schema.Ref = nil
|
||||
schema.AllOf = []jsonschema.SchemaOrBool{(&jsonschema.Schema{}).WithRef(ref).ToSchemaOrBool()}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (openapi *OpenAPI) CreateAndWrite(path string) error {
|
||||
if err := openapi.apiserver.Router().Walk(openapi.collector.Walker); err != nil {
|
||||
return err
|
||||
|
||||
46
pkg/signoz/openapi_test.go
Normal file
46
pkg/signoz/openapi_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package signoz
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
type describedKind string
|
||||
|
||||
func (describedKind) Enum() []any { return []any{"a", "b"} }
|
||||
|
||||
type describedNested struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type describedParent struct {
|
||||
Kind describedKind `json:"kind" description:"The kind of thing."`
|
||||
Nested describedNested `json:"nested" description:"The nested thing."`
|
||||
Undescribed describedKind `json:"undescribed"`
|
||||
Plain string `json:"plain" description:"A plain string."`
|
||||
}
|
||||
|
||||
func TestKeepDescriptionOnReferencedProperty(t *testing.T) {
|
||||
reflector := jsonschema.Reflector{}
|
||||
reflector.DefaultOptions = append(reflector.DefaultOptions, jsonschema.InterceptProp(keepDescriptionOnReferencedProperty))
|
||||
|
||||
schema, err := reflector.Reflect(describedParent{})
|
||||
require.NoError(t, err)
|
||||
|
||||
raw, err := json.Marshal(schema)
|
||||
require.NoError(t, err)
|
||||
|
||||
var got struct {
|
||||
Properties map[string]json.RawMessage `json:"properties"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(raw, &got))
|
||||
|
||||
assert.JSONEq(t, `{"description":"The kind of thing.","allOf":[{"$ref":"#/definitions/SignozDescribedKind"}]}`, string(got.Properties["kind"]))
|
||||
assert.JSONEq(t, `{"description":"The nested thing.","allOf":[{"$ref":"#/definitions/SignozDescribedNested"}]}`, string(got.Properties["nested"]))
|
||||
assert.JSONEq(t, `{"$ref":"#/definitions/SignozDescribedKind"}`, string(got.Properties["undescribed"]))
|
||||
assert.JSONEq(t, `{"description":"A plain string.","type":"string"}`, string(got.Properties["plain"]))
|
||||
}
|
||||
@@ -262,6 +262,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddRuleViewFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddFieldTuplesFactory(sqlstore),
|
||||
sqlmigration.NewKeepRelevantLLMPricingRulesFactory(),
|
||||
sqlmigration.NewBackfillTelegramChannelSpecFactory(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
188
pkg/sqlmigration/136_backfill_telegram_channel_spec.go
Normal file
188
pkg/sqlmigration/136_backfill_telegram_channel_spec.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type backfillTelegramChannelSpec struct {
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewBackfillTelegramChannelSpecFactory() factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("backfill_telegram_channel_spec"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &backfillTelegramChannelSpec{logger: ps.Logger}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *backfillTelegramChannelSpec) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
// Up fills the spec of telegram channels add_channel_spec left NULL because v2
|
||||
// did not model telegram then. parse_mode is not carried: the spec has no field
|
||||
// for it and every send goes out as HTML. Rows carrying any other notifier are
|
||||
// left to the repair endpoint.
|
||||
func (migration *backfillTelegramChannelSpec) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
rows := make([]*channelSpecBackfillRow, 0)
|
||||
if err := tx.NewSelect().Model(&rows).Where("spec IS NULL").OrderExpr("org_id, id").Scan(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
kind, spec, err := telegramChannelSpecFromReceiverJSON(row.Data)
|
||||
if kind != "telegram" {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
migration.logger.WarnContext(ctx, "leaving telegram notification channel without a v2 spec", slog.String("org_id", row.OrgID), slog.String("channel_id", row.ID), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
|
||||
encoded, err := marshalUnescaped(spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.NewUpdate().
|
||||
Model((*channelSpecBackfillRow)(nil)).
|
||||
Set("spec = ?", string(encoded)).
|
||||
Set("type = ?", kind).
|
||||
Where("id = ?", row.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
migration.logger.InfoContext(ctx, "filled v2 spec on telegram notification channel", slog.String("org_id", row.OrgID), slog.String("channel_id", row.ID))
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *backfillTelegramChannelSpec) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// telegramChannelSpecFromReceiverJSON reads the kind from the receiver rather
|
||||
// than the stored type, which v1 could leave empty.
|
||||
func telegramChannelSpecFromReceiverJSON(data string) (string, map[string]any, error) {
|
||||
receiver := map[string]json.RawMessage{}
|
||||
if err := json.Unmarshal([]byte(data), &receiver); err != nil {
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
total := 0
|
||||
var telegrams []notifierJSON
|
||||
for key, raw := range receiver {
|
||||
if !strings.HasSuffix(key, "_configs") {
|
||||
continue
|
||||
}
|
||||
|
||||
var list []notifierJSON
|
||||
if err := json.Unmarshal(raw, &list); err != nil {
|
||||
if key != "telegram_configs" {
|
||||
return "", nil, nil
|
||||
}
|
||||
return "telegram", nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "%s", key)
|
||||
}
|
||||
total += len(list)
|
||||
if key == "telegram_configs" {
|
||||
telegrams = list
|
||||
}
|
||||
}
|
||||
|
||||
if len(telegrams) == 0 {
|
||||
return "", nil, nil
|
||||
}
|
||||
if total > 1 {
|
||||
return "telegram", nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "carries %d notifier configurations; only one per channel is supported", total)
|
||||
}
|
||||
|
||||
spec, err := convertTelegramNotifierJSON(telegrams[0])
|
||||
return "telegram", spec, err
|
||||
}
|
||||
|
||||
func convertTelegramNotifierJSON(notifier notifierJSON) (map[string]any, error) {
|
||||
if err := rejectAnyHTTPAuthJSON(notifier); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, key := range []string{"token_file", "chat_file"} {
|
||||
value, err := notifier.stringValue(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if value != "" {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "telegram: %s is not supported", key)
|
||||
}
|
||||
}
|
||||
|
||||
spec := map[string]any{}
|
||||
if err := notifier.copyStrings(spec, map[string]string{"token": "botToken"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := notifier.copyNonEmptyStrings(spec, map[string]string{"api_url": "apiUrl", "message": "message"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if spec["botToken"] == "" {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "telegram: token is required")
|
||||
}
|
||||
|
||||
chatID, err := notifier.int64Value("chat")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if chatID == 0 {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "telegram: chat is required")
|
||||
}
|
||||
spec["chatId"] = chatID
|
||||
|
||||
threadID, err := notifier.int64Value("message_thread_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if threadID != 0 {
|
||||
spec["messageThreadId"] = threadID
|
||||
}
|
||||
|
||||
disableNotifications, err := notifier.boolValue("disable_notifications")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if disableNotifications {
|
||||
spec["disableNotifications"] = true
|
||||
}
|
||||
|
||||
sendResolved, err := notifier.boolValue("send_resolved")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spec["sendResolved"] = sendResolved
|
||||
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func (n notifierJSON) int64Value(key string) (int64, error) {
|
||||
if !n.has(key) {
|
||||
return 0, nil
|
||||
}
|
||||
var value int64
|
||||
if err := json.Unmarshal(n[key], &value); err != nil {
|
||||
return 0, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "%s", key)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user