mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-10 23:10:47 +01:00
Compare commits
32 Commits
issue_5601
...
tvats-in-o
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d633701c4 | ||
|
|
1a293652f7 | ||
|
|
0f3fb71067 | ||
|
|
b4f5b3eddf | ||
|
|
b9f4fcd681 | ||
|
|
0dd9a156b9 | ||
|
|
f44d6c7c84 | ||
|
|
90d280d871 | ||
|
|
1ab44d4244 | ||
|
|
cd8346a91a | ||
|
|
db5f4b4cd5 | ||
|
|
3bd9b2a96a | ||
|
|
c4e6487e0d | ||
|
|
40aa322cc3 | ||
|
|
2a2b393146 | ||
|
|
b904aca1a8 | ||
|
|
530c050b71 | ||
|
|
bfa174ce2a | ||
|
|
85bf5ce644 | ||
|
|
a654f648ee | ||
|
|
a81c7d3f97 | ||
|
|
b2ff5ef99c | ||
|
|
58c21637a1 | ||
|
|
80fd5cc38a | ||
|
|
fa05a73aef | ||
|
|
38cc4d2bea | ||
|
|
e0b278e8e2 | ||
|
|
a711cda7ba | ||
|
|
e08ef01170 | ||
|
|
00b7ecbd71 | ||
|
|
7243560d8d | ||
|
|
53ab4546bc |
11
.claude/rules/comments.md
Normal file
11
.claude/rules/comments.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Comments
|
||||
|
||||
Applies to everything in the repo — code, config, workflows.
|
||||
|
||||
- **No unnecessary comments.** Do not comment where the code is self-explanatory; never restate what the code already says.
|
||||
- **Document only** non-obvious behavior, constraints, formats, and edge cases.
|
||||
- **Rationale goes in prose, not source.** Why a version is pinned, why a job exists, how a subsystem fits together — that belongs in the README or the PR.
|
||||
- **Never remove pre-existing comments** when editing code. The bar above applies to comments you write, not comments already there.
|
||||
- **Never talk to the reviewer.** No comments about where a change came from, what was changed, or why the change is correct — that belongs in the PR description and is noise the moment it merges.
|
||||
|
||||
Language rules build on this one: [`go-comments`](go-comments.md), [`py-comments`](py-comments.md).
|
||||
12
.claude/rules/go-comments.md
Normal file
12
.claude/rules/go-comments.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.go"
|
||||
---
|
||||
|
||||
# Go comments
|
||||
|
||||
The bar is the [`comments`](comments.md) rule: nothing where the code is self-explanatory.
|
||||
|
||||
- **Names carry the meaning.** Make function, type, and variable names self-explanatory so the comment is unnecessary in the first place. If a comment is needed to explain what a function does, fix the name, not the comment.
|
||||
- **Godoc**: Skip comments that merely restate the identifier. Document only non-obvious behavior, constraints, formats, and edge cases.
|
||||
- **Generated code**: If the comment is emitted by an external codegen tool, leave it as-is — do not add or trim comments in generated files.
|
||||
7
.claude/rules/pull-requests.md
Normal file
7
.claude/rules/pull-requests.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Pull requests
|
||||
|
||||
- **Follow the template** (`.github/pull_request_template.md`): fill in its headings (Description / Issues closed by this PR / Screenshots / Additional Information). Don't add sections the template doesn't have.
|
||||
- **Keep only the headings that apply.** Delete every heading that has nothing under it, along with its `<!--...-->` placeholder comment. The body must never contain an empty heading — if only Description applies, the body has exactly that one heading.
|
||||
- **Keep the description concise and human-readable.** A few plain bullets saying what changed and why, for a reviewer skimming it — not a wall of text, not a restatement of the diff, not generated boilerplate.
|
||||
- **Reference issues with `Closes #issue-number`** under "Issues closed by this PR" so they auto-close on merge. This goes in the PR description only — never in commit messages.
|
||||
- **AI assistance in commits may optionally be disclosed with an `Assisted-by:` trailer** naming the model (e.g. `Assisted-by: Claude Opus 4.5`) — do NOT use a `Co-authored-by:` trailer for this.
|
||||
13
.claude/rules/py-comments.md
Normal file
13
.claude/rules/py-comments.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.py"
|
||||
---
|
||||
|
||||
# Python comments
|
||||
|
||||
The bar is the [`comments`](comments.md) rule: nothing where the code is self-explanatory.
|
||||
|
||||
- **Names carry the meaning.** Make function and variable names self-explanatory so the comment or docstring is unnecessary in the first place. If a docstring is needed to explain what a function does, fix the name, not the docstring.
|
||||
- **No file-level docstring.** The filename says what the module is for — `tool_bin.py` gets the tool binary. A module docstring restating that is noise, and a paragraph of design prose at the top of a file goes stale where nobody is looking. A constraint belongs next to the code it constrains, not in a preamble.
|
||||
- **Docstrings**: only when they say something the name and signature don't — drop them otherwise. Keep them short. A contract that genuinely needs a few lines (interacting flags, retry semantics, an edge case) is fine; a narrative is not.
|
||||
- **No song and dance.** Comment the constraint or the edge case. Not the narrative, not the rationale, not what the next line does.
|
||||
19
.claude/rules/pytest.md
Normal file
19
.claude/rules/pytest.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
paths:
|
||||
- "tests/**/*.py"
|
||||
---
|
||||
|
||||
# pytest conventions
|
||||
|
||||
For the Python integration suite under `tests/`. Setup, running, and suite layout live in [`docs/contributing/tests/integration.md`](../../docs/contributing/tests/integration.md).
|
||||
|
||||
- **No `_`-prefixed helper functions in test modules — this is the rule that matters most.** A reader must be able to see what a test does in its body alone, without chasing private helpers that scatter the meaning across the file. Inline the logic: an expression, a comprehension, a few repeated lines are all fine — repetition across tests is cheaper than indirection. When several tests genuinely share non-trivial setup or assertions, that is what fixtures are for — in `tests/fixtures/`, see the next rule. A module-level `_helper()` is never the answer.
|
||||
- **Fixtures live in `tests/fixtures/` — never under `integration/tests/`.** Not in test modules, not in suite `conftest.py` files. `tests/fixtures/` is the shared library (auth, signoz, clickhouse, logs/metrics/traces seeding, …): reuse what's there before writing anything new; when a new fixture is genuinely needed, add it to the matching `tests/fixtures/` module and register new modules in `tests/conftest.py` `pytest_plugins`. **The one exception: SigNoz-level fixtures in a suite's `conftest.py`.** A suite that needs its own SigNoz spun up with different envs (`create_signoz`/`create_migrator` with `env_overrides` + `cache_key` — e.g. basepath, metricreduction, querier_json_body) keeps that in its `conftest.py`; that is always okay.
|
||||
- **Fixture only when there is a lifecycle; otherwise a plain function.** A fixture earns its indirection by owning setup/teardown (`yield` + cleanup — `insert_metrics` truncating on teardown) or by provisioning a resource (containers, SigNoz instances). A stateless action or lookup (`create_saved_view`, `find_saved_view_by_name`, wiping a resource list) is a plain importable function in the matching `tests/fixtures/` module, taking `signoz`/`token` as ordinary arguments — never wrap a plain callable in a fixture-factory just to inject `signoz`.
|
||||
- **Fixtures own their cleanup.** When a test needs seeded state, put the seed + cleanup pair in a fixture (`yield`, then tear down) so tests in the same suite don't interfere — the pattern `insert_metrics` sets: yield a callable, truncate on teardown.
|
||||
- **Fixture-factory over indirect parametrization.** A fixture that yields a callable (e.g. `insert_metrics(metrics)`) is clearer than `@pytest.mark.parametrize(..., indirect=True)` + `request.param` — the value is an explicit argument, not resolved by magic.
|
||||
- **Skip at collection, not inside the test body.** Use `pytest.param(..., marks=pytest.mark.skip(reason="…"))` so a skipped case shows as SKIPPED-with-reason **and** short-circuits before its fixtures run (no environment spin-up for a test that won't execute).
|
||||
- **Test config comes from explicit `--flags`, not the environment.** Wire configuration as pytest options declared in `tests/conftest.py` (`pytest_addoption` — e.g. `--sqlstore-provider`, `--clickhouse-version`); do **not** add `os.environ` fallbacks inside tests or fixtures.
|
||||
- **snake_case parametrize ids.** `ids=["fill_gaps", "fill_zero"]`, not camelCase.
|
||||
- **Name suite files with the two-digit prefix (`NN_*.py`).** `pyproject.toml` restricts collection to `[0-9][0-9]_*.py` (plus the bootstrap `setup.py` / `run.py`) — a file that doesn't match is silently never collected.
|
||||
- **Always run pytest from `tests/`.** `--import-mode=importlib` is what allows same-basename files across suites (`querier/01_logs.py` vs `rawexportdata/01_logs.py`), but it disables pytest's implicit `sys.path` injection — `import fixtures` only resolves via `pythonpath = ["."]` from that rootdir.
|
||||
88
.github/pull_request_template.md
vendored
88
.github/pull_request_template.md
vendored
@@ -1,85 +1,13 @@
|
||||
## Pull Request
|
||||
|
||||
---
|
||||
|
||||
### 📄 Summary
|
||||
> Why does this change exist?
|
||||
> What problem does it solve, and why is this the right approach?
|
||||
|
||||
|
||||
|
||||
#### Screenshots / Screen Recordings (if applicable)
|
||||
> Include screenshots or screen recordings that clearly show the behavior before the change and the result after the change. This helps reviewers quickly understand the impact and verify the update.
|
||||
|
||||
<!--A few plain bullets saying what changed and why, for a reviewer skimming it - not a wall of text, not a restatement of the diff, not generated boilerplate.-->
|
||||
#### Description
|
||||
|
||||
<!--Reference issues using `Closes #issue-number` to enable automatic closure on merge. -->
|
||||
#### Issues closed by this PR
|
||||
> Reference issues using `Closes #issue-number` to enable automatic closure on merge.
|
||||
|
||||
---
|
||||
<!--If applicable, include screenshots or screen recordings that clearly show the behavior before the change and the result after the change. -->
|
||||
#### Screenshots / Screen Recordings
|
||||
|
||||
### ✅ Change Type
|
||||
_Select all that apply_
|
||||
<!--Anything reviewers should keep in mind while reviewing -->
|
||||
#### Additional Information
|
||||
|
||||
- [ ] ✨ Feature
|
||||
- [ ] 🐛 Bug fix
|
||||
- [ ] ♻️ Refactor
|
||||
- [ ] 🛠️ Infra / Tooling
|
||||
- [ ] 🧪 Test-only
|
||||
|
||||
---
|
||||
|
||||
### 🐛 Bug Context
|
||||
> Required if this PR fixes a bug
|
||||
|
||||
#### Root Cause
|
||||
> What caused the issue?
|
||||
> Regression, faulty assumption, edge case, refactor, etc.
|
||||
|
||||
#### Fix Strategy
|
||||
> How does this PR address the root cause?
|
||||
|
||||
---
|
||||
|
||||
### 🧪 Testing Strategy
|
||||
> How was this change validated?
|
||||
|
||||
- Tests added/updated:
|
||||
- Manual verification:
|
||||
- Edge cases covered:
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ Risk & Impact Assessment
|
||||
> What could break? How do we recover?
|
||||
|
||||
- Blast radius:
|
||||
- Potential regressions:
|
||||
- Rollback plan:
|
||||
|
||||
---
|
||||
|
||||
### 📝 Changelog
|
||||
> Fill only if this affects users, APIs, UI, or documented behavior
|
||||
> Use **N/A** for internal or non-user-facing changes
|
||||
|
||||
| Field | Value |
|
||||
|------|-------|
|
||||
| Deployment Type | Cloud / OSS / Enterprise |
|
||||
| Change Type | Feature / Bug Fix / Maintenance |
|
||||
| Description | User-facing summary |
|
||||
|
||||
---
|
||||
|
||||
### 📋 Checklist
|
||||
- [ ] Tests added or explicitly not required
|
||||
- [ ] Manually tested
|
||||
- [ ] Breaking changes documented
|
||||
- [ ] Backward compatibility considered
|
||||
|
||||
---
|
||||
|
||||
## 👀 Notes for Reviewers
|
||||
|
||||
<!-- Anything reviewers should keep in mind while reviewing -->
|
||||
|
||||
---
|
||||
<!--Please delete paragraphs that you did not use before submitting.-->
|
||||
|
||||
83
.github/workflows/docs.yml
vendored
83
.github/workflows/docs.yml
vendored
@@ -1,83 +0,0 @@
|
||||
name: "Update PR labels and Block PR until related docs are shipped for the feature"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
types: [opened, edited, labeled, unlabeled]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
docs_label_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR Title and Manage Labels
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const prTitle = context.payload.pull_request.title;
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
// Fetch the current PR details to get labels
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
const labels = pr.data.labels.map(label => label.name);
|
||||
|
||||
if (prTitle.startsWith('feat:')) {
|
||||
const hasDocsRequired = labels.includes('docs required');
|
||||
const hasDocsShipped = labels.includes('docs shipped');
|
||||
const hasDocsNotRequired = labels.includes('docs not required');
|
||||
|
||||
// If "docs not required" is present, skip the checks
|
||||
if (hasDocsNotRequired && !hasDocsRequired) {
|
||||
console.log("Skipping checks due to 'docs not required' label.");
|
||||
return; // Exit the script early
|
||||
}
|
||||
|
||||
// If "docs shipped" is present, remove "docs required" if it exists
|
||||
if (hasDocsShipped && hasDocsRequired) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
name: 'docs required'
|
||||
});
|
||||
console.log("Removed 'docs required' label.");
|
||||
}
|
||||
|
||||
// Add "docs required" label if neither "docs shipped" nor "docs required" are present
|
||||
if (!hasDocsRequired && !hasDocsShipped) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
labels: ['docs required']
|
||||
});
|
||||
console.log("Added 'docs required' label.");
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the updated labels after any changes
|
||||
const updatedPr = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
const updatedLabels = updatedPr.data.labels.map(label => label.name);
|
||||
const updatedHasDocsRequired = updatedLabels.includes('docs required');
|
||||
const updatedHasDocsShipped = updatedLabels.includes('docs shipped');
|
||||
|
||||
// Block PR if "docs required" is still present and "docs shipped" is missing
|
||||
if (updatedHasDocsRequired && !updatedHasDocsShipped) {
|
||||
core.setFailed("This PR requires documentation. Please remove the 'docs required' label and add the 'docs shipped' label to proceed.");
|
||||
}
|
||||
15
.github/workflows/goci.yaml
vendored
15
.github/workflows/goci.yaml
vendored
@@ -53,6 +53,21 @@ jobs:
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_VERSION: 1.24
|
||||
semconv-generated:
|
||||
if: |
|
||||
github.event_name == 'merge_group' ||
|
||||
(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'))
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: self-checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: go-install
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
- name: check-semconv-generated-files
|
||||
run: go run ./scripts/semconv -check
|
||||
build:
|
||||
if: |
|
||||
github.event_name == 'merge_group' ||
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -90,8 +90,6 @@ queries.active
|
||||
.devenv/**/tmp/**
|
||||
.qodo
|
||||
|
||||
.dev
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
@@ -231,4 +229,6 @@ cython_debug/
|
||||
# LSP config files
|
||||
pyrightconfig.json
|
||||
|
||||
|
||||
# dev
|
||||
.dev/
|
||||
.claude/worktrees/
|
||||
|
||||
4
Makefile
4
Makefile
@@ -233,6 +233,10 @@ py-clean: ## Clear all pycache and pytest cache from tests directory recursively
|
||||
##############################################################
|
||||
# generate commands
|
||||
##############################################################
|
||||
.PHONY: semconv-generate
|
||||
semconv-generate: ## Regenerate semantic-convention families for Go and TypeScript
|
||||
@go run ./scripts/semconv
|
||||
|
||||
.PHONY: gen-mocks
|
||||
gen-mocks:
|
||||
@echo ">> Generating mocks"
|
||||
|
||||
@@ -4212,6 +4212,21 @@ components:
|
||||
- missingOptionalMetrics
|
||||
- missingRequiredAttributes
|
||||
type: object
|
||||
InframonitoringtypesClusterFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByNodeReadiness:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
|
||||
nullable: true
|
||||
type: array
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesClusterRecord:
|
||||
properties:
|
||||
clusterCPU:
|
||||
@@ -4349,6 +4364,16 @@ components:
|
||||
- containerCannotRun
|
||||
- unknown
|
||||
type: object
|
||||
InframonitoringtypesContainerFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByContainerStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesContainerStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesContainerReady:
|
||||
enum:
|
||||
- ready
|
||||
@@ -4448,6 +4473,16 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesDaemonSetFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesDaemonSetRecord:
|
||||
properties:
|
||||
currentNodes:
|
||||
@@ -4520,6 +4555,16 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesDeploymentFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesDeploymentRecord:
|
||||
properties:
|
||||
availablePods:
|
||||
@@ -4661,6 +4706,16 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesJobFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesJobRecord:
|
||||
properties:
|
||||
activePods:
|
||||
@@ -4784,6 +4839,16 @@ components:
|
||||
- message
|
||||
- documentationLink
|
||||
type: object
|
||||
InframonitoringtypesNamespaceFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesNamespaceRecord:
|
||||
properties:
|
||||
counts:
|
||||
@@ -4865,6 +4930,21 @@ components:
|
||||
- ready
|
||||
- notReady
|
||||
type: object
|
||||
InframonitoringtypesNodeFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByNodeReadiness:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
|
||||
nullable: true
|
||||
type: array
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesNodeRecord:
|
||||
properties:
|
||||
condition:
|
||||
@@ -4981,6 +5061,16 @@ components:
|
||||
- shutdown
|
||||
- unexpectedAdmissionError
|
||||
type: object
|
||||
InframonitoringtypesPodFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesPodRecord:
|
||||
properties:
|
||||
meta:
|
||||
@@ -5080,7 +5170,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesClusterFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5106,7 +5196,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesContainerFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5132,7 +5222,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesDaemonSetFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5158,7 +5248,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesDeploymentFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5210,7 +5300,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesJobFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5236,7 +5326,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesNamespaceFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5262,7 +5352,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5288,7 +5378,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5314,7 +5404,7 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
$ref: '#/components/schemas/InframonitoringtypesStatefulSetFilter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
@@ -5365,6 +5455,16 @@ components:
|
||||
- list
|
||||
- grouped_list
|
||||
type: string
|
||||
InframonitoringtypesStatefulSetFilter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
filterByPodStatus:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
InframonitoringtypesStatefulSetRecord:
|
||||
properties:
|
||||
currentPods:
|
||||
@@ -7759,6 +7859,112 @@ components:
|
||||
enum:
|
||||
- basic
|
||||
type: string
|
||||
SavedviewtypesDisplay:
|
||||
properties:
|
||||
color:
|
||||
type: string
|
||||
fontSize:
|
||||
type: string
|
||||
format:
|
||||
type: string
|
||||
maxLines:
|
||||
type: integer
|
||||
type: object
|
||||
SavedviewtypesPanelType:
|
||||
enum:
|
||||
- value
|
||||
- graph
|
||||
- table
|
||||
- list
|
||||
- trace
|
||||
type: string
|
||||
SavedviewtypesPostableSavedView:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
generateName:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
required:
|
||||
- source
|
||||
- data
|
||||
type: object
|
||||
SavedviewtypesSavedView:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
SavedviewtypesSavedViewData:
|
||||
properties:
|
||||
schemaVersion:
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
SavedviewtypesSavedViewSpec:
|
||||
properties:
|
||||
display:
|
||||
$ref: '#/components/schemas/SavedviewtypesDisplay'
|
||||
displayName:
|
||||
type: string
|
||||
panelType:
|
||||
$ref: '#/components/schemas/SavedviewtypesPanelType'
|
||||
queries:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryEnvelope'
|
||||
type: array
|
||||
selectedFields:
|
||||
items:
|
||||
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
|
||||
type: array
|
||||
required:
|
||||
- displayName
|
||||
- panelType
|
||||
- queries
|
||||
- selectedFields
|
||||
- display
|
||||
type: object
|
||||
SavedviewtypesSource:
|
||||
enum:
|
||||
- traces
|
||||
- logs
|
||||
- metrics
|
||||
- meter
|
||||
type: string
|
||||
SavedviewtypesUpdatableSavedView:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
required:
|
||||
- source
|
||||
- data
|
||||
type: object
|
||||
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
|
||||
properties:
|
||||
id:
|
||||
@@ -8732,15 +8938,6 @@ components:
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
TypesPostableBulkInviteRequest:
|
||||
properties:
|
||||
invites:
|
||||
items:
|
||||
$ref: '#/components/schemas/TypesPostableInvite'
|
||||
type: array
|
||||
required:
|
||||
- invites
|
||||
type: object
|
||||
TypesPostableForgotPassword:
|
||||
properties:
|
||||
email:
|
||||
@@ -11348,57 +11545,6 @@ paths:
|
||||
summary: Create invite
|
||||
tags:
|
||||
- users
|
||||
/api/v1/invite/bulk:
|
||||
post:
|
||||
deprecated: true
|
||||
description: This endpoint creates a bulk invite for a user
|
||||
operationId: CreateBulkInvite
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TypesPostableBulkInviteRequest'
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Create bulk invite
|
||||
tags:
|
||||
- users
|
||||
/api/v1/llm_pricing_rules:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -12052,9 +12198,9 @@ paths:
|
||||
- dashboard
|
||||
/api/v1/resetPassword:
|
||||
post:
|
||||
deprecated: false
|
||||
deprecated: true
|
||||
description: This endpoint resets the password by token
|
||||
operationId: ResetPassword
|
||||
operationId: ResetPasswordDeprecated
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
@@ -14322,177 +14468,6 @@ paths:
|
||||
summary: List users
|
||||
tags:
|
||||
- users
|
||||
/api/v1/user/{id}:
|
||||
delete:
|
||||
deprecated: true
|
||||
description: This endpoint deletes the user by id
|
||||
operationId: DeleteUserDeprecated
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Delete user
|
||||
tags:
|
||||
- users
|
||||
get:
|
||||
deprecated: true
|
||||
description: This endpoint returns the user by id
|
||||
operationId: GetUserDeprecated
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/TypesDeprecatedUser'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Get user
|
||||
tags:
|
||||
- users
|
||||
put:
|
||||
deprecated: true
|
||||
description: This endpoint updates the user by id
|
||||
operationId: UpdateUserDeprecated
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TypesDeprecatedUser'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/TypesDeprecatedUser'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Update user
|
||||
tags:
|
||||
- users
|
||||
/api/v1/user/me:
|
||||
get:
|
||||
deprecated: true
|
||||
@@ -15592,6 +15567,41 @@ paths:
|
||||
summary: Forgot password
|
||||
tags:
|
||||
- users
|
||||
/api/v2/factor_password/reset:
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint resets the password using a single use reset password
|
||||
token
|
||||
operationId: ResetPassword
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TypesPostableResetPassword'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
summary: Reset password
|
||||
tags:
|
||||
- users
|
||||
/api/v2/features:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -22659,6 +22669,298 @@ paths:
|
||||
summary: Test alert rule
|
||||
tags:
|
||||
- rules
|
||||
/api/v2/saved_views:
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns saved views, optionally filtered by source and name.
|
||||
operationId: ListSavedViews
|
||||
parameters:
|
||||
- in: query
|
||||
name: source
|
||||
schema:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
- in: query
|
||||
name: name
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedView'
|
||||
nullable: true
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- saved-view:list
|
||||
- tokenizer:
|
||||
- saved-view:list
|
||||
summary: List saved views
|
||||
tags:
|
||||
- saved_view
|
||||
post:
|
||||
deprecated: false
|
||||
description: Persists a saved view for the explore page. Returns the id of the
|
||||
created view.
|
||||
operationId: CreateSavedView
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SavedviewtypesPostableSavedView'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/TypesIdentifiable'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: Created
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- saved-view:create
|
||||
- tokenizer:
|
||||
- saved-view:create
|
||||
summary: Create saved view
|
||||
tags:
|
||||
- saved_view
|
||||
/api/v2/saved_views/{id}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: Deletes a saved view by id.
|
||||
operationId: DeleteSavedView
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- saved-view:delete
|
||||
- tokenizer:
|
||||
- saved-view:delete
|
||||
summary: Delete saved view
|
||||
tags:
|
||||
- saved_view
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns a saved view by id.
|
||||
operationId: GetSavedView
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedView'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- saved-view:read
|
||||
- tokenizer:
|
||||
- saved-view:read
|
||||
summary: Get saved view
|
||||
tags:
|
||||
- saved_view
|
||||
put:
|
||||
deprecated: false
|
||||
description: Replaces a saved view's name and query.
|
||||
operationId: UpdateSavedView
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SavedviewtypesUpdatableSavedView'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- saved-view:update
|
||||
- tokenizer:
|
||||
- saved-view:update
|
||||
summary: Update saved view
|
||||
tags:
|
||||
- saved_view
|
||||
/api/v2/sessions:
|
||||
delete:
|
||||
deprecated: false
|
||||
|
||||
@@ -349,7 +349,7 @@ func (Step) JSONSchema() (jsonschema.Schema, error) {
|
||||
|
||||
### `oneOf` with a discriminator
|
||||
|
||||
For a sum type whose variants are keyed by a property (e.g. `kind`), expose the variants via `JSONSchemaOneOf()` and add a discriminator. Without it, code generators intersect the variants (`A & B & C`) instead of producing a clean discriminated union (`A | B | C`).
|
||||
For a sum type whose variants are keyed by a property (e.g. `kind`), expose the variants via `JSONSchemaOneOf()` and add a discriminator. Without it, code generators intersect the variants (`A & B & C`) instead of producing a clean discriminated union (`A | B | C`). How to model the sum type itself is covered in [types.md](types.md#sum-types-the-kindspec-envelope) — this section is only about its schema.
|
||||
|
||||
The parent keeps its `JSONSchemaOneOf()` (the `oneOf` itself) and *additionally* tags it via `PrepareJSONSchema` with the `x-signoz-discriminator` extension; `signoz.attachDiscriminators` then promotes that marker to a real OpenAPI 3 `discriminator` (and strips the duplicate parent properties) after reflection.
|
||||
|
||||
|
||||
@@ -99,6 +99,69 @@ Each flavor exists for a concrete reason:
|
||||
|
||||
The core `AuthDomain` holds the two live halves — `storableAuthDomain` and `authDomainConfig` — and owns business methods such as `Update(config)`. Conversions use the `New<Output>From<Input>` form: `NewAuthDomainFromConfig`, `NewAuthDomainFromStorableAuthDomain`, `NewGettableAuthDomainFromAuthDomain`.
|
||||
|
||||
## Sum types: the kind/spec envelope
|
||||
|
||||
When a domain type is a *sum type* — exactly one of several variants, selected by a discriminator — model it as an envelope with a `kind` and a `spec`:
|
||||
|
||||
```go
|
||||
type FooConfig struct {
|
||||
Kind FooKind `json:"kind" required:"true"`
|
||||
Spec any `json:"spec" required:"true"`
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{ "kind": "bar", "spec": { "url": "...", "timeout": "30s" } }
|
||||
```
|
||||
|
||||
`Kind` is a `valuer.String` enum implementing `Enum()`; `Spec` holds exactly one concrete variant type (`BarSpec`, `BazSpec`, …). `RuleThresholdData` and `EvaluationEnvelope` in `pkg/types/ruletypes/` are the canonical in-tree examples; the dashboard panel/query/variable plugins in `pkg/types/dashboardtypes/` are the same pattern behind generics. (`QueryEnvelope` in querybuildertypes uses `type` as the discriminator key for historical reasons; new envelopes use `kind`.)
|
||||
|
||||
### The envelope goes at the point of variance, not the resource root
|
||||
|
||||
Put the envelope on the field that actually varies. The resource root is almost never a sum type — a `Foo` has a `name` and an `enabled` flag regardless of which kind it is configured with; only its configuration varies, so the envelope is the `config` field:
|
||||
|
||||
```json
|
||||
{ "name": "my-foo", "enabled": true, "config": { "kind": "bar", "spec": { "...": "..." } } }
|
||||
```
|
||||
|
||||
Hoisting `kind`/`spec` to the root would turn the whole resource into a `oneOf`: every flavor (`PostableFoo`, `UpdatableFoo`, `GettableFoo`) then needs one variant schema per kind, each repeating the common fields; every new common field has to be added to all of them; and generated clients get unions of large objects instead of one small union that narrows on `config.kind`. A root-level `kind` also collides with the resource-model meaning of the word — root `kind` conventionally answers "what resource is this" (`Dashboard`), never "which flavor of config does it hold".
|
||||
|
||||
The existing domains already follow this placement:
|
||||
|
||||
- **Rules** — plain root; envelopes on the varying fields: `thresholds: {kind, spec}` and `evaluation: {kind, spec}`.
|
||||
- **Dashboards** — metadata at the root plus one typed `spec`; the unions sit deep inside, at each panel/query/variable plugin (`{kind, spec}` in `perses_plugin_wrappers.go`).
|
||||
- **Saved views** — root `{schemaVersion, spec}`, where `spec` is a *versioning* envelope holding one fixed type, not a union; the unions are inside it (`spec.queries: [{type, spec}]`). Same word, different job — a versioned body is not a discriminated union.
|
||||
|
||||
### Why this tagging style
|
||||
|
||||
Of the union encodings in common use, the envelope is the *adjacently tagged* one — tag and payload side by side. Variant payloads stay collision-free, and each kind maps to a named wrapper schema that carries the discriminator, which is exactly what OpenAPI generators need. The alternatives lose on those points: *internally tagged* (`{"kind": "bar", ...fields flattened}`) mixes common and variant fields, admits cross-variant key collisions, and forces every variant schema to redeclare the discriminator; *sibling optional fields* (`{"kind": "bar", "barConfig": {}, "bazConfig": {}}`) is the anti-pattern the first rule below exists to prevent.
|
||||
|
||||
The rules that make the envelope work:
|
||||
|
||||
- **Never model variants as sibling fields.** A struct with `Bar *BarSpec`, `Baz *BazSpec` next to a discriminator cannot be expressed as an OpenAPI discriminated union, forces nilability checks on every consumer, and silently admits contradictory payloads (kind=bar with a baz spec). The chosen variant *is* the payload.
|
||||
- **The envelope owns `UnmarshalJSON`.** Decode `kind` first, then switch on it to decode and validate the matching concrete type into `Spec`. Unknown kinds and missing specs are rejected at the boundary:
|
||||
|
||||
```go
|
||||
func (typ *FooConfig) UnmarshalJSON(data []byte) error {
|
||||
var raw map[string]json.RawMessage
|
||||
// ... unmarshal raw, decode raw["kind"] ...
|
||||
switch kind {
|
||||
case FooKindBar:
|
||||
spec := BarSpec{}
|
||||
if err := json.Unmarshal(raw["spec"], &spec); err != nil {
|
||||
return err
|
||||
}
|
||||
typ.Spec = spec
|
||||
// ... one case per kind, default rejects ...
|
||||
}
|
||||
typ.Kind = kind
|
||||
return nil
|
||||
}
|
||||
```
|
||||
- **Consumers type-assert on `Spec`** (`config.Spec.(BarSpec)`) after switching on `Kind`. If assertion sites multiply, add typed accessors on the envelope (see `EvaluationEnvelope.GetEvaluation()`).
|
||||
- **OpenAPI needs one unexported variant struct per kind** (`fooConfigBar{Kind; Spec BarSpec}`), exposed via `JSONSchemaOneOf()` and mapped via `PrepareJSONSchema` with the `x-signoz-discriminator` extension. The schema mechanics are covered in [handler.md](handler.md#oneof-with-a-discriminator).
|
||||
- **A legacy persisted shape gets a data migration or a `StorableX`.** When rows were written before the envelope existed, prefer an idempotent `sqlmigration` that rewrites them into the new shape, so the storable type simply nests the envelope. Only when the old shape must keep being written (external writers, rollback windows) keep it in a storable twin and convert at the type boundary.
|
||||
|
||||
## Conventions that tie the flavors together
|
||||
|
||||
- **Conversions** use either a `New<Output>From<Input>` constructor — e.g. `NewChannelFromReceiver`, `NewGettableAuthDomainFromAuthDomain` — or a receiver-style `ToY()` method. Both forms coexist in the codebase; use whichever fits the call site.
|
||||
@@ -139,6 +202,8 @@ Both are optional. Do not introduce them if `PostableX` already covers the case.
|
||||
|
||||
- Every domain package defines the core type `X`. Only `X` is mandatory.
|
||||
- Add `PostableX` / `GettableX` / `UpdatableX` / `StorableX` one at a time, only when the shape actually diverges from `X`.
|
||||
- Model sum types as a `{kind, spec}` envelope with a validating `UnmarshalJSON` — never as sibling variant fields next to a discriminator.
|
||||
- The envelope goes on the field that varies, never at the resource root — common fields stay on the resource, outside the union.
|
||||
- Domain logic lives on `X`, not on the flavor types.
|
||||
- Conversions can be a `New<Output>From<Input>` constructor or a receiver-style `ToY()` method — pick whichever reads best at the call site.
|
||||
- Use a type alias when two shapes are truly identical.
|
||||
|
||||
@@ -376,7 +376,19 @@ function App(): JSX.Element {
|
||||
tracesSampleRate: 0, // Ref: https://github.com/SigNoz/platform-pod/issues/2393#issuecomment-4603658055
|
||||
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
|
||||
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
|
||||
beforeSend(event) {
|
||||
beforeSend(event, hint) {
|
||||
const error = hint?.originalException as
|
||||
| { name?: string; code?: string | number }
|
||||
| undefined;
|
||||
|
||||
// Ignore benign aborted/cancelled requests (axios + fetch).
|
||||
if (error?.code === 'ERR_CANCELED' || error?.code === 'ECONNABORTED') {
|
||||
return null;
|
||||
}
|
||||
if (error?.name === 'AbortError') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Drop the event if its level is 'warning' or 'info'
|
||||
if (event.level === 'warning' || event.level === 'info') {
|
||||
return null;
|
||||
|
||||
@@ -527,6 +527,13 @@ const routes: AppRoutes[] = [
|
||||
key: 'AI_OBSERVABILITY_OVERVIEW',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.AI_OBSERVABILITY_EXPLORER,
|
||||
exact: true,
|
||||
component: LLMObservabilityPage,
|
||||
key: 'AI_OBSERVABILITY_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.AI_OBSERVABILITY_CONFIGURATION,
|
||||
exact: true,
|
||||
|
||||
490
frontend/src/api/generated/services/saved-view/index.ts
Normal file
490
frontend/src/api/generated/services/saved-view/index.ts
Normal file
@@ -0,0 +1,490 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
CreateSavedView201,
|
||||
DeleteSavedViewPathParameters,
|
||||
GetSavedView200,
|
||||
GetSavedViewPathParameters,
|
||||
ListSavedViews200,
|
||||
ListSavedViewsParams,
|
||||
RenderErrorResponseDTO,
|
||||
SavedviewtypesPostableSavedViewDTO,
|
||||
SavedviewtypesUpdatableSavedViewDTO,
|
||||
UpdateSavedViewPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* Returns saved views, optionally filtered by source and name.
|
||||
* @summary List saved views
|
||||
*/
|
||||
export const listSavedViews = (
|
||||
params?: ListSavedViewsParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListSavedViews200>({
|
||||
url: `/api/v2/saved_views`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListSavedViewsQueryKey = (params?: ListSavedViewsParams) => {
|
||||
return [`/api/v2/saved_views`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getListSavedViewsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listSavedViews>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params?: ListSavedViewsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listSavedViews>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListSavedViewsQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listSavedViews>>> = ({
|
||||
signal,
|
||||
}) => listSavedViews(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listSavedViews>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListSavedViewsQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listSavedViews>>
|
||||
>;
|
||||
export type ListSavedViewsQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List saved views
|
||||
*/
|
||||
|
||||
export function useListSavedViews<
|
||||
TData = Awaited<ReturnType<typeof listSavedViews>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params?: ListSavedViewsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listSavedViews>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListSavedViewsQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List saved views
|
||||
*/
|
||||
export const invalidateListSavedViews = async (
|
||||
queryClient: QueryClient,
|
||||
params?: ListSavedViewsParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListSavedViewsQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Persists a saved view for the explore page. Returns the id of the created view.
|
||||
* @summary Create saved view
|
||||
*/
|
||||
export const createSavedView = (
|
||||
savedviewtypesPostableSavedViewDTO?: BodyType<SavedviewtypesPostableSavedViewDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateSavedView201>({
|
||||
url: `/api/v2/saved_views`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: savedviewtypesPostableSavedViewDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateSavedViewMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createSavedView>>,
|
||||
TError,
|
||||
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createSavedView>>,
|
||||
TError,
|
||||
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createSavedView'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createSavedView>>,
|
||||
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createSavedView(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateSavedViewMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createSavedView>>
|
||||
>;
|
||||
export type CreateSavedViewMutationBody =
|
||||
| BodyType<SavedviewtypesPostableSavedViewDTO>
|
||||
| undefined;
|
||||
export type CreateSavedViewMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create saved view
|
||||
*/
|
||||
export const useCreateSavedView = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createSavedView>>,
|
||||
TError,
|
||||
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createSavedView>>,
|
||||
TError,
|
||||
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateSavedViewMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Deletes a saved view by id.
|
||||
* @summary Delete saved view
|
||||
*/
|
||||
export const deleteSavedView = (
|
||||
{ id }: DeleteSavedViewPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v2/saved_views/${id}`,
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteSavedViewMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteSavedView>>,
|
||||
TError,
|
||||
{ pathParams: DeleteSavedViewPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteSavedView>>,
|
||||
TError,
|
||||
{ pathParams: DeleteSavedViewPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteSavedView'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof deleteSavedView>>,
|
||||
{ pathParams: DeleteSavedViewPathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteSavedView(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteSavedViewMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteSavedView>>
|
||||
>;
|
||||
|
||||
export type DeleteSavedViewMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete saved view
|
||||
*/
|
||||
export const useDeleteSavedView = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteSavedView>>,
|
||||
TError,
|
||||
{ pathParams: DeleteSavedViewPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteSavedView>>,
|
||||
TError,
|
||||
{ pathParams: DeleteSavedViewPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getDeleteSavedViewMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Returns a saved view by id.
|
||||
* @summary Get saved view
|
||||
*/
|
||||
export const getSavedView = (
|
||||
{ id }: GetSavedViewPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetSavedView200>({
|
||||
url: `/api/v2/saved_views/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetSavedViewQueryKey = ({ id }: GetSavedViewPathParameters) => {
|
||||
return [`/api/v2/saved_views/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetSavedViewQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getSavedView>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetSavedViewPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSavedView>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetSavedViewQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getSavedView>>> = ({
|
||||
signal,
|
||||
}) => getSavedView({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSavedView>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetSavedViewQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getSavedView>>
|
||||
>;
|
||||
export type GetSavedViewQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get saved view
|
||||
*/
|
||||
|
||||
export function useGetSavedView<
|
||||
TData = Awaited<ReturnType<typeof getSavedView>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetSavedViewPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSavedView>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetSavedViewQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get saved view
|
||||
*/
|
||||
export const invalidateGetSavedView = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetSavedViewPathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetSavedViewQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces a saved view's name and query.
|
||||
* @summary Update saved view
|
||||
*/
|
||||
export const updateSavedView = (
|
||||
{ id }: UpdateSavedViewPathParameters,
|
||||
savedviewtypesUpdatableSavedViewDTO?: BodyType<SavedviewtypesUpdatableSavedViewDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v2/saved_views/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: savedviewtypesUpdatableSavedViewDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateSavedViewMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSavedView>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSavedViewPathParameters;
|
||||
data?: BodyType<SavedviewtypesUpdatableSavedViewDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSavedView>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSavedViewPathParameters;
|
||||
data?: BodyType<SavedviewtypesUpdatableSavedViewDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateSavedView'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof updateSavedView>>,
|
||||
{
|
||||
pathParams: UpdateSavedViewPathParameters;
|
||||
data?: BodyType<SavedviewtypesUpdatableSavedViewDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateSavedView(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateSavedViewMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateSavedView>>
|
||||
>;
|
||||
export type UpdateSavedViewMutationBody =
|
||||
| BodyType<SavedviewtypesUpdatableSavedViewDTO>
|
||||
| undefined;
|
||||
export type UpdateSavedViewMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update saved view
|
||||
*/
|
||||
export const useUpdateSavedView = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSavedView>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSavedViewPathParameters;
|
||||
data?: BodyType<SavedviewtypesUpdatableSavedViewDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateSavedView>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSavedViewPathParameters;
|
||||
data?: BodyType<SavedviewtypesUpdatableSavedViewDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateSavedViewMutationOptions(options));
|
||||
};
|
||||
@@ -5648,6 +5648,47 @@ export interface InframonitoringtypesChecksDTO {
|
||||
type: InframonitoringtypesCheckTypeDTO;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesNodeConditionDTO {
|
||||
ready = 'ready',
|
||||
not_ready = 'not_ready',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export enum InframonitoringtypesPodStatusDTO {
|
||||
pending = 'pending',
|
||||
running = 'running',
|
||||
failed = 'failed',
|
||||
unknown = 'unknown',
|
||||
crashloopbackoff = 'crashloopbackoff',
|
||||
imagepullbackoff = 'imagepullbackoff',
|
||||
errimagepull = 'errimagepull',
|
||||
createcontainerconfigerror = 'createcontainerconfigerror',
|
||||
containercreating = 'containercreating',
|
||||
oomkilled = 'oomkilled',
|
||||
completed = 'completed',
|
||||
error = 'error',
|
||||
containercannotrun = 'containercannotrun',
|
||||
evicted = 'evicted',
|
||||
nodeaffinity = 'nodeaffinity',
|
||||
nodelost = 'nodelost',
|
||||
shutdown = 'shutdown',
|
||||
unexpectedadmissionerror = 'unexpectedadmissionerror',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export interface InframonitoringtypesClusterFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO[] | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesClusterRecordDTOCounts = {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -5923,21 +5964,6 @@ export interface InframonitoringtypesContainerCountsByStatusDTO {
|
||||
waiting: number;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesContainerReadyDTO {
|
||||
ready = 'ready',
|
||||
not_ready = 'not_ready',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export type InframonitoringtypesContainerRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesContainerRecordDTOMeta =
|
||||
InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
|
||||
|
||||
export enum InframonitoringtypesContainerStatusDTO {
|
||||
running = 'running',
|
||||
waiting = 'waiting',
|
||||
@@ -5954,6 +5980,32 @@ export enum InframonitoringtypesContainerStatusDTO {
|
||||
unknown = 'unknown',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export interface InframonitoringtypesContainerFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByContainerStatus?: InframonitoringtypesContainerStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesContainerReadyDTO {
|
||||
ready = 'ready',
|
||||
not_ready = 'not_ready',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export type InframonitoringtypesContainerRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesContainerRecordDTOMeta =
|
||||
InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesContainerRecordDTO {
|
||||
containerCountsByReady: InframonitoringtypesContainerCountsByReadyDTO;
|
||||
containerCountsByStatus: InframonitoringtypesContainerCountsByStatusDTO;
|
||||
@@ -6025,6 +6077,17 @@ export interface InframonitoringtypesContainersDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDaemonSetFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesDaemonSetRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6110,6 +6173,17 @@ export interface InframonitoringtypesDaemonSetsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDeploymentFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesDeploymentRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6272,6 +6346,17 @@ export interface InframonitoringtypesHostsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesJobFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesJobRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6357,6 +6442,17 @@ export interface InframonitoringtypesJobsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesNamespaceFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesNamespaceRecordDTOCounts = {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -6433,11 +6529,21 @@ export interface InframonitoringtypesNamespacesDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesNodeConditionDTO {
|
||||
ready = 'ready',
|
||||
not_ready = 'not_ready',
|
||||
no_data = 'no_data',
|
||||
export interface InframonitoringtypesNodeFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO[] | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesNodeRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6499,6 +6605,17 @@ export interface InframonitoringtypesNodesDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPodFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6509,27 +6626,6 @@ export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
|
||||
export type InframonitoringtypesPodRecordDTOMeta =
|
||||
InframonitoringtypesPodRecordDTOMetaAnyOf | null;
|
||||
|
||||
export enum InframonitoringtypesPodStatusDTO {
|
||||
pending = 'pending',
|
||||
running = 'running',
|
||||
failed = 'failed',
|
||||
unknown = 'unknown',
|
||||
crashloopbackoff = 'crashloopbackoff',
|
||||
imagepullbackoff = 'imagepullbackoff',
|
||||
errimagepull = 'errimagepull',
|
||||
createcontainerconfigerror = 'createcontainerconfigerror',
|
||||
containercreating = 'containercreating',
|
||||
oomkilled = 'oomkilled',
|
||||
completed = 'completed',
|
||||
error = 'error',
|
||||
containercannotrun = 'containercannotrun',
|
||||
evicted = 'evicted',
|
||||
nodeaffinity = 'nodeaffinity',
|
||||
nodelost = 'nodelost',
|
||||
shutdown = 'shutdown',
|
||||
unexpectedadmissionerror = 'unexpectedadmissionerror',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export interface InframonitoringtypesPodRecordDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
@@ -6606,7 +6702,7 @@ export interface InframonitoringtypesPostableClustersDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesClusterFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6633,7 +6729,7 @@ export interface InframonitoringtypesPostableContainersDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesContainerFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6660,7 +6756,7 @@ export interface InframonitoringtypesPostableDaemonSetsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesDaemonSetFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6687,7 +6783,7 @@ export interface InframonitoringtypesPostableDeploymentsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesDeploymentFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6741,7 +6837,7 @@ export interface InframonitoringtypesPostableJobsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesJobFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6768,7 +6864,7 @@ export interface InframonitoringtypesPostableNamespacesDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesNamespaceFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6795,7 +6891,7 @@ export interface InframonitoringtypesPostableNodesDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesNodeFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6822,7 +6918,7 @@ export interface InframonitoringtypesPostablePodsDTO {
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesPodFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -6843,13 +6939,24 @@ export interface InframonitoringtypesPostablePodsDTO {
|
||||
start: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesStatefulSetFilterDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
expression?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPostableStatefulSetsDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
filter?: InframonitoringtypesStatefulSetFilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
@@ -8858,6 +8965,112 @@ export interface RuletypesRuleDTO {
|
||||
export enum RuletypesThresholdKindDTO {
|
||||
basic = 'basic',
|
||||
}
|
||||
export interface SavedviewtypesDisplayDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
color?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
fontSize?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
format?: string;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
maxLines?: number;
|
||||
}
|
||||
|
||||
export enum SavedviewtypesPanelTypeDTO {
|
||||
value = 'value',
|
||||
graph = 'graph',
|
||||
table = 'table',
|
||||
list = 'list',
|
||||
trace = 'trace',
|
||||
}
|
||||
export interface SavedviewtypesSavedViewSpecDTO {
|
||||
display: SavedviewtypesDisplayDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
displayName: string;
|
||||
panelType: SavedviewtypesPanelTypeDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
queries: Querybuildertypesv5QueryEnvelopeDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
selectedFields: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
}
|
||||
|
||||
export interface SavedviewtypesSavedViewDataDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
schemaVersion: string;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export enum SavedviewtypesSourceDTO {
|
||||
traces = 'traces',
|
||||
logs = 'logs',
|
||||
metrics = 'metrics',
|
||||
meter = 'meter',
|
||||
}
|
||||
export interface SavedviewtypesPostableSavedViewDTO {
|
||||
data: SavedviewtypesSavedViewDataDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
generateName?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
source: SavedviewtypesSourceDTO;
|
||||
}
|
||||
|
||||
export interface SavedviewtypesSavedViewDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
data?: SavedviewtypesSavedViewDataDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
source?: SavedviewtypesSourceDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface SavedviewtypesUpdatableSavedViewDTO {
|
||||
data: SavedviewtypesSavedViewDataDTO;
|
||||
source: SavedviewtypesSourceDTO;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -9844,6 +10057,21 @@ export interface TypesOrganizationDTO {
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface TypesPostableForgotPasswordDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
email: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
frontendBaseURL?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId: string;
|
||||
}
|
||||
|
||||
export interface TypesPostableInviteDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -9863,28 +10091,6 @@ export interface TypesPostableInviteDTO {
|
||||
role?: string;
|
||||
}
|
||||
|
||||
export interface TypesPostableBulkInviteRequestDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
invites: TypesPostableInviteDTO[];
|
||||
}
|
||||
|
||||
export interface TypesPostableForgotPasswordDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
email: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
frontendBaseURL?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId: string;
|
||||
}
|
||||
|
||||
export interface TypesPostableResetPasswordDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -10995,31 +11201,6 @@ export type ListUsersDeprecated200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteUserDeprecatedPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetUserDeprecatedPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetUserDeprecated200 = {
|
||||
data: TypesDeprecatedUserDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateUserDeprecatedPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type UpdateUserDeprecated200 = {
|
||||
data: TypesDeprecatedUserDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetMyUserDeprecated200 = {
|
||||
data: TypesDeprecatedUserDTO;
|
||||
/**
|
||||
@@ -12056,6 +12237,54 @@ export type TestRule200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListSavedViewsParams = {
|
||||
/**
|
||||
* @description undefined
|
||||
*/
|
||||
source?: SavedviewtypesSourceDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type ListSavedViews200 = {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
data: SavedviewtypesSavedViewDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateSavedView201 = {
|
||||
data: TypesIdentifiableDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteSavedViewPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetSavedViewPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetSavedView200 = {
|
||||
data: SavedviewtypesSavedViewDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateSavedViewPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetSessionContext200 = {
|
||||
data: AuthtypesSessionContextDTO;
|
||||
/**
|
||||
|
||||
@@ -25,7 +25,6 @@ import type {
|
||||
CreateResetPasswordTokenPathParameters,
|
||||
CreateUser201,
|
||||
CreateUserRole201,
|
||||
DeleteUserDeprecatedPathParameters,
|
||||
DeleteUserPathParameters,
|
||||
DeleteUserRolePathParameters,
|
||||
GetMyUser200,
|
||||
@@ -37,8 +36,6 @@ import type {
|
||||
GetRolesByUserID200,
|
||||
GetRolesByUserIDPathParameters,
|
||||
GetUser200,
|
||||
GetUserDeprecated200,
|
||||
GetUserDeprecatedPathParameters,
|
||||
GetUserPathParameters,
|
||||
GetUserRole200,
|
||||
GetUserRolePathParameters,
|
||||
@@ -50,16 +47,12 @@ import type {
|
||||
RenderErrorResponseDTO,
|
||||
SetRoleByUserIDPathParameters,
|
||||
TypesChangePasswordRequestDTO,
|
||||
TypesDeprecatedUserDTO,
|
||||
TypesPostableBulkInviteRequestDTO,
|
||||
TypesPostableForgotPasswordDTO,
|
||||
TypesPostableInviteDTO,
|
||||
TypesPostableResetPasswordDTO,
|
||||
TypesPostableRoleDTO,
|
||||
TypesPostableVerifyResetPasswordTokenDTO,
|
||||
TypesUpdatableUserDTO,
|
||||
UpdateUserDeprecated200,
|
||||
UpdateUserDeprecatedPathParameters,
|
||||
UpdateUserPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
@@ -260,96 +253,12 @@ export const useCreateInvite = <
|
||||
> => {
|
||||
return useMutation(getCreateInviteMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint creates a bulk invite for a user
|
||||
* @deprecated
|
||||
* @summary Create bulk invite
|
||||
*/
|
||||
export const createBulkInvite = (
|
||||
typesPostableBulkInviteRequestDTO?: BodyType<TypesPostableBulkInviteRequestDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/invite/bulk`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: typesPostableBulkInviteRequestDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateBulkInviteMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createBulkInvite>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createBulkInvite>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createBulkInvite'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createBulkInvite>>,
|
||||
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createBulkInvite(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateBulkInviteMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createBulkInvite>>
|
||||
>;
|
||||
export type CreateBulkInviteMutationBody =
|
||||
| BodyType<TypesPostableBulkInviteRequestDTO>
|
||||
| undefined;
|
||||
export type CreateBulkInviteMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Create bulk invite
|
||||
*/
|
||||
export const useCreateBulkInvite = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createBulkInvite>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createBulkInvite>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateBulkInviteMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint resets the password by token
|
||||
* @deprecated
|
||||
* @summary Reset password
|
||||
*/
|
||||
export const resetPassword = (
|
||||
export const resetPasswordDeprecated = (
|
||||
typesPostableResetPasswordDTO?: BodyType<TypesPostableResetPasswordDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
@@ -362,23 +271,23 @@ export const resetPassword = (
|
||||
});
|
||||
};
|
||||
|
||||
export const getResetPasswordMutationOptions = <
|
||||
export const getResetPasswordDeprecatedMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof resetPassword>>,
|
||||
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableResetPasswordDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof resetPassword>>,
|
||||
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableResetPasswordDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['resetPassword'];
|
||||
const mutationKey = ['resetPasswordDeprecated'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
@@ -388,45 +297,47 @@ export const getResetPasswordMutationOptions = <
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof resetPassword>>,
|
||||
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
|
||||
{ data?: BodyType<TypesPostableResetPasswordDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return resetPassword(data);
|
||||
return resetPasswordDeprecated(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type ResetPasswordMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof resetPassword>>
|
||||
export type ResetPasswordDeprecatedMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof resetPasswordDeprecated>>
|
||||
>;
|
||||
export type ResetPasswordMutationBody =
|
||||
export type ResetPasswordDeprecatedMutationBody =
|
||||
| BodyType<TypesPostableResetPasswordDTO>
|
||||
| undefined;
|
||||
export type ResetPasswordMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
export type ResetPasswordDeprecatedMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Reset password
|
||||
*/
|
||||
export const useResetPassword = <
|
||||
export const useResetPasswordDeprecated = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof resetPassword>>,
|
||||
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableResetPasswordDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof resetPassword>>,
|
||||
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableResetPasswordDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getResetPasswordMutationOptions(options));
|
||||
return useMutation(getResetPasswordDeprecatedMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint lists all users
|
||||
@@ -515,295 +426,6 @@ export const invalidateListUsersDeprecated = async (
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint deletes the user by id
|
||||
* @deprecated
|
||||
* @summary Delete user
|
||||
*/
|
||||
export const deleteUserDeprecated = (
|
||||
{ id }: DeleteUserDeprecatedPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/user/${id}`,
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteUserDeprecatedMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteUserDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteUserDeprecatedPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteUserDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteUserDeprecatedPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteUserDeprecated'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof deleteUserDeprecated>>,
|
||||
{ pathParams: DeleteUserDeprecatedPathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteUserDeprecated(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteUserDeprecatedMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteUserDeprecated>>
|
||||
>;
|
||||
|
||||
export type DeleteUserDeprecatedMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Delete user
|
||||
*/
|
||||
export const useDeleteUserDeprecated = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteUserDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteUserDeprecatedPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteUserDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteUserDeprecatedPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getDeleteUserDeprecatedMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint returns the user by id
|
||||
* @deprecated
|
||||
* @summary Get user
|
||||
*/
|
||||
export const getUserDeprecated = (
|
||||
{ id }: GetUserDeprecatedPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetUserDeprecated200>({
|
||||
url: `/api/v1/user/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetUserDeprecatedQueryKey = ({
|
||||
id,
|
||||
}: GetUserDeprecatedPathParameters) => {
|
||||
return [`/api/v1/user/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetUserDeprecatedQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getUserDeprecated>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetUserDeprecatedPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getUserDeprecated>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetUserDeprecatedQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getUserDeprecated>>
|
||||
> = ({ signal }) => getUserDeprecated({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getUserDeprecated>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetUserDeprecatedQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getUserDeprecated>>
|
||||
>;
|
||||
export type GetUserDeprecatedQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Get user
|
||||
*/
|
||||
|
||||
export function useGetUserDeprecated<
|
||||
TData = Awaited<ReturnType<typeof getUserDeprecated>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetUserDeprecatedPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getUserDeprecated>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetUserDeprecatedQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Get user
|
||||
*/
|
||||
export const invalidateGetUserDeprecated = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetUserDeprecatedPathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetUserDeprecatedQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint updates the user by id
|
||||
* @deprecated
|
||||
* @summary Update user
|
||||
*/
|
||||
export const updateUserDeprecated = (
|
||||
{ id }: UpdateUserDeprecatedPathParameters,
|
||||
typesDeprecatedUserDTO?: BodyType<TypesDeprecatedUserDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<UpdateUserDeprecated200>({
|
||||
url: `/api/v1/user/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: typesDeprecatedUserDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateUserDeprecatedMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateUserDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateUserDeprecatedPathParameters;
|
||||
data?: BodyType<TypesDeprecatedUserDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateUserDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateUserDeprecatedPathParameters;
|
||||
data?: BodyType<TypesDeprecatedUserDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateUserDeprecated'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof updateUserDeprecated>>,
|
||||
{
|
||||
pathParams: UpdateUserDeprecatedPathParameters;
|
||||
data?: BodyType<TypesDeprecatedUserDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateUserDeprecated(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateUserDeprecatedMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateUserDeprecated>>
|
||||
>;
|
||||
export type UpdateUserDeprecatedMutationBody =
|
||||
| BodyType<TypesDeprecatedUserDTO>
|
||||
| undefined;
|
||||
export type UpdateUserDeprecatedMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Update user
|
||||
*/
|
||||
export const useUpdateUserDeprecated = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateUserDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateUserDeprecatedPathParameters;
|
||||
data?: BodyType<TypesDeprecatedUserDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateUserDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateUserDeprecatedPathParameters;
|
||||
data?: BodyType<TypesDeprecatedUserDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateUserDeprecatedMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint returns the user I belong to
|
||||
* @deprecated
|
||||
@@ -974,6 +596,89 @@ export const useForgotPassword = <
|
||||
> => {
|
||||
return useMutation(getForgotPasswordMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint resets the password using a single use reset password token
|
||||
* @summary Reset password
|
||||
*/
|
||||
export const resetPassword = (
|
||||
typesPostableResetPasswordDTO?: BodyType<TypesPostableResetPasswordDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v2/factor_password/reset`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: typesPostableResetPasswordDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getResetPasswordMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof resetPassword>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableResetPasswordDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof resetPassword>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableResetPasswordDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['resetPassword'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof resetPassword>>,
|
||||
{ data?: BodyType<TypesPostableResetPasswordDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return resetPassword(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type ResetPasswordMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof resetPassword>>
|
||||
>;
|
||||
export type ResetPasswordMutationBody =
|
||||
| BodyType<TypesPostableResetPasswordDTO>
|
||||
| undefined;
|
||||
export type ResetPasswordMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Reset password
|
||||
*/
|
||||
export const useResetPassword = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof resetPassword>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableResetPasswordDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof resetPassword>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableResetPasswordDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getResetPasswordMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint verifies whether a reset password token exists and is not expired
|
||||
* @summary Verify a reset password token
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/user/resetPassword';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useResetPassword` hook (or `resetPassword` fetcher) from
|
||||
* `api/generated/services/users` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
const resetPassword = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>(`/resetPassword`, {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default resetPassword;
|
||||
@@ -1,28 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { UsersProps } from 'types/api/user/inviteUsers';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useCreateBulkInvite` hook (or `createBulkInvite` fetcher) from
|
||||
* `api/generated/services/users` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
const inviteUsers = async (
|
||||
users: UsersProps,
|
||||
): Promise<SuccessResponseV2<null>> => {
|
||||
try {
|
||||
const response = await axios.post(`/invite/bulk`, users);
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: null,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default inviteUsers;
|
||||
1
frontend/src/assets/Logos/gcp.svg
Normal file
1
frontend/src/assets/Logos/gcp.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 128 128"><path fill="#ea4535" d="M80.6 40.3h.4l-.2-.2 14-14v-.3c-11.8-10.4-28.1-14-43.2-9.5C36.5 20.8 24.9 32.8 20.7 48c.2-.1.5-.2.8-.2 5.2-3.4 11.4-5.4 17.9-5.4 2.2 0 4.3.2 6.4.6.1-.1.2-.1.3-.1 9-9.9 24.2-11.1 34.6-2.6h-.1z"/><path fill="#557ebf" d="M108.1 47.8c-2.3-8.5-7.1-16.2-13.8-22.1L80 39.9c6 4.9 9.5 12.3 9.3 20v2.5c16.9 0 16.9 25.2 0 25.2H63.9v20h-.1l.1.2h25.4c14.6.1 27.5-9.3 31.8-23.1 4.3-13.8-1-28.8-13-36.9z"/><path fill="#36a852" d="M39 107.9h26.3V87.7H39c-1.9 0-3.7-.4-5.4-1.1l-15.2 14.6v.2c6 4.3 13.2 6.6 20.7 6.6z"/><path fill="#f9bc15" d="M40.2 41.9c-14.9.1-28.1 9.3-32.9 22.8-4.8 13.6 0 28.5 11.8 37.3l15.6-14.9c-8.6-3.7-10.6-14.5-4-20.8 6.6-6.4 17.8-4.4 21.7 3.8L68 55.2C61.4 46.9 51.1 42 40.2 42.1z"/></svg>
|
||||
|
After Width: | Height: | Size: 805 B |
@@ -23,6 +23,13 @@
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
.cloud-service-data-collected-table-heading-info {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--l3-foreground);
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-service-data-collected-table-logs {
|
||||
@@ -32,3 +39,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-service-data-collected-table-tooltip {
|
||||
max-width: 280px;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@@ -3,16 +3,19 @@ import {
|
||||
CloudintegrationtypesCollectedLogAttributeDTO,
|
||||
CloudintegrationtypesCollectedMetricDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { BarChart, ScrollText } from '@signozhq/icons';
|
||||
import { BarChart, Info, ScrollText } from '@signozhq/icons';
|
||||
import { TooltipProvider, TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
|
||||
import './CloudServiceDataCollected.styles.scss';
|
||||
|
||||
function CloudServiceDataCollected({
|
||||
logsData,
|
||||
metricsData,
|
||||
metricsInfoTooltip,
|
||||
}: {
|
||||
logsData: CloudintegrationtypesCollectedLogAttributeDTO[] | null | undefined;
|
||||
metricsData: CloudintegrationtypesCollectedMetricDTO[] | null | undefined;
|
||||
metricsInfoTooltip?: string;
|
||||
}): JSX.Element {
|
||||
const logsColumns = [
|
||||
{
|
||||
@@ -84,6 +87,25 @@ function CloudServiceDataCollected({
|
||||
<div className="cloud-service-data-collected-table-heading">
|
||||
<BarChart size={14} />
|
||||
Metrics
|
||||
{metricsInfoTooltip && (
|
||||
<TooltipProvider>
|
||||
<TooltipSimple
|
||||
title={metricsInfoTooltip}
|
||||
side="top"
|
||||
tooltipContentProps={{
|
||||
className: 'cloud-service-data-collected-table-tooltip',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="cloud-service-data-collected-table-heading-info"
|
||||
aria-label="About the metrics listed below"
|
||||
data-testid="data-collected-metrics-info"
|
||||
>
|
||||
<Info size={12} />
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
<Table
|
||||
columns={metricsColumns}
|
||||
@@ -97,4 +119,8 @@ function CloudServiceDataCollected({
|
||||
);
|
||||
}
|
||||
|
||||
CloudServiceDataCollected.defaultProps = {
|
||||
metricsInfoTooltip: undefined,
|
||||
};
|
||||
|
||||
export default CloudServiceDataCollected;
|
||||
|
||||
@@ -80,7 +80,7 @@ function InviteMembers({
|
||||
weight="semibold"
|
||||
className={styles.headerCellRole}
|
||||
>
|
||||
Role
|
||||
Roles
|
||||
</Typography.Text>
|
||||
<div className={styles.headerCellAction} />
|
||||
</div>
|
||||
@@ -108,11 +108,10 @@ function InviteMembers({
|
||||
|
||||
<div className={styles.cellRole}>
|
||||
<RolesSelect
|
||||
mode="single"
|
||||
value={row.roleId || undefined}
|
||||
onChange={(roleId): void => updateRole(row.id, roleId)}
|
||||
placeholder="Select role"
|
||||
allowClear={false}
|
||||
mode="multiple"
|
||||
value={row.roleIds}
|
||||
onChange={(roleIds): void => updateRole(row.id, roleIds)}
|
||||
placeholder="Select roles"
|
||||
id={`invite-role-${row.id}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -68,8 +68,8 @@ describe('InviteMembers - Edge Cases', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
await expect(
|
||||
@@ -100,8 +100,8 @@ describe('InviteMembers - Edge Cases', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
await expect(
|
||||
@@ -132,17 +132,17 @@ describe('InviteMembers - Edge Cases', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
await expect(
|
||||
screen.findByTestId('invite-api-error'),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
const viewerElements = screen.getAllByText('Viewer');
|
||||
const viewerElements = screen.getAllByTitle('Viewer');
|
||||
await user.click(viewerElements[0]);
|
||||
const editorOptions = await screen.findAllByText('Editor');
|
||||
const editorOptions = await screen.findAllByTitle('Editor');
|
||||
await user.click(editorOptions[editorOptions.length - 1]);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -189,8 +189,8 @@ describe('InviteMembers - Edge Cases', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
const submitBtn = screen.getByTestId('submit-btn');
|
||||
await user.click(submitBtn);
|
||||
@@ -226,8 +226,8 @@ describe('InviteMembers - Edge Cases', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], ' alice@signoz.io ');
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
|
||||
@@ -32,14 +32,14 @@ describe('InviteMembers - Rendering', () => {
|
||||
render(<InviteMembers />);
|
||||
|
||||
expect(screen.getByText('Email address')).toBeInTheDocument();
|
||||
expect(screen.getByText('Role')).toBeInTheDocument();
|
||||
expect(screen.getByText('Roles')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides header when showHeader is false', () => {
|
||||
render(<InviteMembers showHeader={false} />);
|
||||
|
||||
expect(screen.queryByText('Email address')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Role')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Roles')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders add button by default', () => {
|
||||
@@ -89,7 +89,7 @@ describe('InviteMembers - Rendering', () => {
|
||||
it('renders role select for each row', () => {
|
||||
render(<InviteMembers initialRowCount={2} />);
|
||||
|
||||
const roleSelects = screen.getAllByText('Select role');
|
||||
const roleSelects = screen.getAllByText('Select roles');
|
||||
expect(roleSelects).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,8 +40,8 @@ describe('InviteMembers - Submission', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], 'alice@signoz.io');
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -73,17 +73,17 @@ describe('InviteMembers - Submission', () => {
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
|
||||
await user.type(emailInputs[0], 'alice@signoz.io');
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
await user.type(emailInputs[1], 'bob@signoz.io');
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
const editorOptions = await screen.findAllByText('Editor');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
const editorOptions = await screen.findAllByTitle('Editor');
|
||||
await user.click(editorOptions[editorOptions.length - 1]);
|
||||
|
||||
await user.type(emailInputs[2], 'charlie@signoz.io');
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
const adminOptions = await screen.findAllByText('Admin');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
const adminOptions = await screen.findAllByTitle('Admin');
|
||||
await user.click(adminOptions[adminOptions.length - 1]);
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
@@ -125,8 +125,8 @@ describe('InviteMembers - Submission', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -154,8 +154,8 @@ describe('InviteMembers - Submission', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -218,12 +218,12 @@ describe('InviteMembers - Submission', () => {
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
|
||||
await user.type(emailInputs[0], 'alice@signoz.io');
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
await user.type(emailInputs[1], 'bob@signoz.io');
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
const editorOptions = await screen.findAllByText('Editor');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
const editorOptions = await screen.findAllByTitle('Editor');
|
||||
await user.click(editorOptions[editorOptions.length - 1]);
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
@@ -276,8 +276,8 @@ describe('InviteMembers - Submission', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -303,8 +303,8 @@ describe('InviteMembers - Submission', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ describe('InviteMembers - Validation', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], INVALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -60,8 +60,8 @@ describe('InviteMembers - Validation', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], INVALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -85,8 +85,8 @@ describe('InviteMembers - Validation', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], INVALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
await expect(
|
||||
@@ -149,8 +149,8 @@ describe('InviteMembers - Validation', () => {
|
||||
screen.findByText('Please select roles for team members'),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
@@ -204,8 +204,8 @@ describe('InviteMembers - Validation', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
|
||||
export interface InviteMemberRow {
|
||||
id: string;
|
||||
email: string;
|
||||
roleId: string;
|
||||
roleIds: string[];
|
||||
}
|
||||
|
||||
export interface InviteResult {
|
||||
@@ -38,7 +38,7 @@ export interface UseInviteMembersReturn {
|
||||
addRow: () => void;
|
||||
removeRow: (id: string) => void;
|
||||
updateEmail: (id: string, email: string) => void;
|
||||
updateRole: (id: string, roleId: string | undefined) => void;
|
||||
updateRole: (id: string, roleIds: string[]) => void;
|
||||
reset: () => void;
|
||||
submit: () => Promise<InviteResult[]>;
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ import {
|
||||
const createEmptyRow = (): InviteMemberRow => ({
|
||||
id: uuid(),
|
||||
email: '',
|
||||
roleId: '',
|
||||
roleIds: [],
|
||||
});
|
||||
|
||||
const isRowTouched = (row: InviteMemberRow): boolean =>
|
||||
row.email.trim() !== '' || row.roleId !== '';
|
||||
row.email.trim() !== '' || row.roleIds.length > 0;
|
||||
|
||||
export function useInviteMembers(
|
||||
options: UseInviteMembersOptions = {},
|
||||
@@ -78,7 +78,7 @@ export function useInviteMembers(
|
||||
|
||||
touched.forEach((row) => {
|
||||
const emailValid = EMAIL_REGEX.test(row.email);
|
||||
const roleValid = row.roleId !== '';
|
||||
const roleValid = row.roleIds.length > 0;
|
||||
|
||||
if (!emailValid || !row.email) {
|
||||
isValid = false;
|
||||
@@ -139,12 +139,12 @@ export function useInviteMembers(
|
||||
);
|
||||
|
||||
const updateRole = useCallback(
|
||||
(id: string, roleId: string | undefined): void => {
|
||||
(id: string, roleIds: string[]): void => {
|
||||
setRows((prev) => {
|
||||
const updated = cloneDeep(prev);
|
||||
const row = updated.find((r) => r.id === id);
|
||||
if (row) {
|
||||
row.roleId = roleId ?? '';
|
||||
row.roleIds = roleIds;
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
@@ -187,7 +187,7 @@ export function useInviteMembers(
|
||||
await createUser({
|
||||
email: row.email.trim(),
|
||||
frontendBaseUrl: getBaseUrl(),
|
||||
userRoles: [{ id: row.roleId }],
|
||||
userRoles: row.roleIds.map((id) => ({ id })),
|
||||
});
|
||||
results.push({ email: row.email, success: true });
|
||||
} catch (err) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tooltipContent {
|
||||
--tooltip-z-index: 2100;
|
||||
}
|
||||
|
||||
.dropdownContent {
|
||||
--dropdown-menu-content-z-index: 2100;
|
||||
}
|
||||
|
||||
.leftSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-family: 'Geist Mono', monospace;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
color: var(--l1-foreground);
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.arrows {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { DropdownMenuSimple as Dropdown } from '@signozhq/ui/dropdown-menu';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Compass,
|
||||
Copy,
|
||||
Ellipsis,
|
||||
Link,
|
||||
} from '@signozhq/icons';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { MouseEvent, MouseEventHandler } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
import styles from './LogDetailsHeader.module.scss';
|
||||
|
||||
const TOOLTIP_CONTENT_PROPS = { className: styles.tooltipContent };
|
||||
|
||||
interface LogDetailsHeaderProps {
|
||||
log: ILog;
|
||||
onNavigatePrev: () => void;
|
||||
onNavigateNext: () => void;
|
||||
isPrevDisabled: boolean;
|
||||
isNextDisabled: boolean;
|
||||
showOpenInExplorer?: boolean;
|
||||
onOpenInExplorer?: MouseEventHandler;
|
||||
}
|
||||
|
||||
function LogDetailsHeader({
|
||||
log,
|
||||
onNavigatePrev,
|
||||
onNavigateNext,
|
||||
isPrevDisabled,
|
||||
isNextDisabled,
|
||||
showOpenInExplorer = false,
|
||||
onOpenInExplorer,
|
||||
}: LogDetailsHeaderProps): JSX.Element {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const { onLogCopy } = useCopyLogLink(log?.id);
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
const handleCopyLog = (): void => {
|
||||
copyToClipboard(aggregateAttributesResourcesToString(log));
|
||||
toast.success('Copied to clipboard', { position: 'bottom-right' });
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: 'copy-log',
|
||||
label: 'Copy log',
|
||||
icon: <Copy size={14} />,
|
||||
onClick: handleCopyLog,
|
||||
},
|
||||
{
|
||||
key: 'copy-link',
|
||||
label: 'Copy link to log',
|
||||
icon: <Link size={14} />,
|
||||
onClick: (): void => onLogCopy(),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.header} data-log-detail-ignore="true">
|
||||
<div className={styles.leftSection}>
|
||||
<Divider type="vertical" className={styles.divider} />
|
||||
<Typography.Text
|
||||
className={styles.timestamp}
|
||||
data-testid="log-details-header-timestamp"
|
||||
>
|
||||
{formatTimezoneAdjustedTimestamp(
|
||||
log.date ?? log.timestamp,
|
||||
DATE_TIME_FORMATS.DASH_DATETIME,
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{showOpenInExplorer && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<Compass size={16} />}
|
||||
onClick={onOpenInExplorer}
|
||||
>
|
||||
Open in Explorer
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Dropdown
|
||||
menu={{ items: menuItems }}
|
||||
align="end"
|
||||
className={styles.dropdownContent}
|
||||
onClick={(e: MouseEvent): void => e.stopPropagation()}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
prefix={<Ellipsis size={16} />}
|
||||
data-testid="log-details-header-menu"
|
||||
/>
|
||||
</Dropdown>
|
||||
|
||||
<div className={styles.arrows}>
|
||||
<TooltipSimple
|
||||
title="Move to previous log"
|
||||
side="top"
|
||||
open={isPrevDisabled ? false : undefined}
|
||||
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<ChevronUp size={14} />}
|
||||
disabled={isPrevDisabled}
|
||||
onClick={onNavigatePrev}
|
||||
data-testid="log-details-header-prev"
|
||||
/>
|
||||
</TooltipSimple>
|
||||
<TooltipSimple
|
||||
title="Move to next log"
|
||||
side="top"
|
||||
open={isNextDisabled ? false : undefined}
|
||||
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<ChevronDown size={14} />}
|
||||
disabled={isNextDisabled}
|
||||
onClick={onNavigateNext}
|
||||
data-testid="log-details-header-next"
|
||||
/>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LogDetailsHeader.defaultProps = {
|
||||
showOpenInExplorer: false,
|
||||
onOpenInExplorer: undefined,
|
||||
};
|
||||
|
||||
export default LogDetailsHeader;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
interface UseLogNavigationParams {
|
||||
logs?: ILog[];
|
||||
activeLogId: string;
|
||||
onNavigateLog?: (log: ILog) => void;
|
||||
onScrollToLog?: (id: string) => void;
|
||||
}
|
||||
|
||||
interface UseLogNavigationReturn {
|
||||
goToPrev: () => void;
|
||||
goToNext: () => void;
|
||||
isPrevDisabled: boolean;
|
||||
isNextDisabled: boolean;
|
||||
}
|
||||
|
||||
export function useLogNavigation({
|
||||
logs,
|
||||
activeLogId,
|
||||
onNavigateLog,
|
||||
onScrollToLog,
|
||||
}: UseLogNavigationParams): UseLogNavigationReturn {
|
||||
const currentIndex = useMemo(
|
||||
() => logs?.findIndex((l) => l.id === activeLogId) ?? -1,
|
||||
[logs, activeLogId],
|
||||
);
|
||||
|
||||
const canNavigate = !!logs?.length && !!onNavigateLog && currentIndex !== -1;
|
||||
const isPrevDisabled = !canNavigate || currentIndex <= 0;
|
||||
const isNextDisabled = !canNavigate || currentIndex >= (logs?.length ?? 0) - 1;
|
||||
|
||||
const goToPrev = useCallback((): void => {
|
||||
if (isPrevDisabled || !logs) {
|
||||
return;
|
||||
}
|
||||
const prev = logs[currentIndex - 1];
|
||||
onNavigateLog?.(prev);
|
||||
onScrollToLog?.(prev.id);
|
||||
}, [isPrevDisabled, logs, currentIndex, onNavigateLog, onScrollToLog]);
|
||||
|
||||
const goToNext = useCallback((): void => {
|
||||
if (isNextDisabled || !logs) {
|
||||
return;
|
||||
}
|
||||
const next = logs[currentIndex + 1];
|
||||
onNavigateLog?.(next);
|
||||
onScrollToLog?.(next.id);
|
||||
}, [isNextDisabled, logs, currentIndex, onNavigateLog, onScrollToLog]);
|
||||
|
||||
return { goToPrev, goToNext, isPrevDisabled, isNextDisabled };
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
.highlights {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px 16px;
|
||||
padding: 12px 0;
|
||||
|
||||
// Constrain each KeyValueLabel (the grid items) to its cell.
|
||||
:global(.key-value-label) {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.valueBadge {
|
||||
--badge-font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// Truncating text inside a badge
|
||||
.badgeText {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.serviceDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-forest);
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.traceLink {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { LOG_HIGHLIGHTS } from './config';
|
||||
import styles from './LogHighlights.module.scss';
|
||||
|
||||
interface LogHighlightsProps {
|
||||
log: ILog;
|
||||
}
|
||||
|
||||
function LogHighlights({ log }: LogHighlightsProps): JSX.Element | null {
|
||||
const fields = LOG_HIGHLIGHTS.map((field) => ({
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
value: field.render(log),
|
||||
})).filter((field) => field.value != null);
|
||||
|
||||
if (fields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.highlights} data-testid="log-details-highlights">
|
||||
{fields.map((field) => (
|
||||
<KeyValueLabel
|
||||
key={field.key}
|
||||
badgeKey={field.label}
|
||||
badgeValue={field.value}
|
||||
direction="column"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogHighlights;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import styles from './LogHighlights.module.scss';
|
||||
|
||||
interface TraceIdFieldProps {
|
||||
traceId: string;
|
||||
}
|
||||
|
||||
function TraceIdField({ traceId }: TraceIdFieldProps): JSX.Element {
|
||||
return (
|
||||
<Link
|
||||
to={{ pathname: `/trace/${traceId}` }}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.traceLink}
|
||||
title={traceId}
|
||||
>
|
||||
{traceId}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default TraceIdField;
|
||||
102
frontend/src/components/LogDetail/LogHighlights/config.tsx
Normal file
102
frontend/src/components/LogDetail/LogHighlights/config.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Badge, BadgeColor } from '@signozhq/ui/badge';
|
||||
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import styles from './LogHighlights.module.scss';
|
||||
import TraceIdField from './TraceIdField';
|
||||
|
||||
// Severity badge color mirrors the LogStateIndicator bar
|
||||
const SEVERITY_COLOR: Record<string, BadgeColor> = {
|
||||
[LogType.TRACE]: 'forest',
|
||||
[LogType.DEBUG]: 'aqua',
|
||||
[LogType.INFO]: 'robin',
|
||||
[LogType.WARN]: 'amber',
|
||||
[LogType.ERROR]: 'cherry',
|
||||
[LogType.FATAL]: 'sakura',
|
||||
};
|
||||
|
||||
export interface LogHighlightConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
render: (log: ILog) => ReactNode | null;
|
||||
}
|
||||
|
||||
// Resource/attribute lookup (keys like `service.name` live in resources_string,
|
||||
// occasionally attributes_string). Typed loosely as these are string maps.
|
||||
const getAttr = (log: ILog, key: string): string =>
|
||||
(log.resources_string as unknown as Record<string, string>)?.[key] ||
|
||||
(log.attributes_string as unknown as Record<string, string>)?.[key] ||
|
||||
'';
|
||||
|
||||
const valueBadge = (
|
||||
value: string,
|
||||
options?: { prefix?: ReactNode; color?: BadgeColor },
|
||||
): ReactNode => (
|
||||
<Badge color={options?.color ?? 'vanilla'} className={styles.valueBadge}>
|
||||
{options?.prefix}
|
||||
<span className={styles.badgeText} title={value}>
|
||||
{value}
|
||||
</span>
|
||||
</Badge>
|
||||
);
|
||||
|
||||
export const LOG_HIGHLIGHTS: LogHighlightConfig[] = [
|
||||
{
|
||||
key: 'service',
|
||||
label: 'SERVICE',
|
||||
render: (log): ReactNode | null => {
|
||||
const value = getAttr(log, 'service.name');
|
||||
return value
|
||||
? valueBadge(value, {
|
||||
prefix: <span className={styles.serviceDot} />,
|
||||
})
|
||||
: null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'severity',
|
||||
label: 'SEVERITY',
|
||||
render: (log): ReactNode | null => {
|
||||
if (!log.severity_text) {
|
||||
return null;
|
||||
}
|
||||
return valueBadge(log.severity_text, {
|
||||
color: SEVERITY_COLOR[getLogIndicatorType(log)] ?? 'vanilla',
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'namespace',
|
||||
label: 'NAMESPACE',
|
||||
render: (log): ReactNode | null => {
|
||||
const value = getAttr(log, 'service.namespace');
|
||||
return value ? valueBadge(value) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'environment',
|
||||
label: 'ENVIRONMENT',
|
||||
render: (log): ReactNode | null => {
|
||||
const value = getAttr(log, 'deployment.environment');
|
||||
return value ? valueBadge(value) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'traceId',
|
||||
label: 'TRACE ID',
|
||||
render: (log): ReactNode | null => {
|
||||
const traceId = log.trace_id || log.traceId;
|
||||
return traceId ? <TraceIdField traceId={traceId} /> : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'spanId',
|
||||
label: 'SPAN ID',
|
||||
render: (log): ReactNode | null => {
|
||||
const spanId = log.span_id || log.spanID;
|
||||
return spanId ? valueBadge(spanId) : null;
|
||||
},
|
||||
},
|
||||
];
|
||||
200
frontend/src/components/LogDetail/__tests__/LogDetail.test.tsx
Normal file
200
frontend/src/components/LogDetail/__tests__/LogDetail.test.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import LogDetail from '..';
|
||||
import { VIEW_TYPES } from '../constants';
|
||||
import { LogDetailProps } from '../LogDetail.interfaces';
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
// The flag to be removed later
|
||||
jest.mock('../constants', () => ({
|
||||
...jest.requireActual('../constants'),
|
||||
isLogDetailsV2: true,
|
||||
}));
|
||||
|
||||
const mockLog: ILog = {
|
||||
id: 'log-1',
|
||||
timestamp: '2024-01-15T09:45:30Z',
|
||||
date: '2024-01-15T09:45:30Z',
|
||||
body: 'test log body',
|
||||
severityText: 'INFO',
|
||||
severityNumber: 9,
|
||||
traceFlags: 0,
|
||||
traceId: '',
|
||||
spanID: '',
|
||||
attributesString: {},
|
||||
attributesInt: {},
|
||||
attributesFloat: {},
|
||||
resources_string: {},
|
||||
scope_string: {},
|
||||
attributes_string: {},
|
||||
severity_text: 'INFO',
|
||||
severity_number: 9,
|
||||
};
|
||||
|
||||
const makeLog = (id: string): ILog => ({ ...mockLog, id });
|
||||
|
||||
function renderDrawer(props: Partial<LogDetailProps> = {}): void {
|
||||
render(
|
||||
<LogDetail
|
||||
log={mockLog}
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
onAddToQuery={jest.fn()}
|
||||
onClickActionItem={jest.fn()}
|
||||
onClose={jest.fn()}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('LogDetail drawer — header (isLogDetailsV2)', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('renders the revamped header when a log is provided', () => {
|
||||
renderDrawer();
|
||||
|
||||
expect(screen.getByTestId('log-details-header-menu')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('log-details-header-prev')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('log-details-header-next')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the log timestamp formatted (DASH_DATETIME) in the header', () => {
|
||||
// Pin the timezone to UTC so the formatted output is deterministic across
|
||||
// machines/CI (Jest doesn't fix a TZ).
|
||||
localStorage.setItem(LOCALSTORAGE.PREFERRED_TIMEZONE, 'UTC');
|
||||
|
||||
renderDrawer();
|
||||
|
||||
// mockLog date is 2024-01-15T09:45:30Z → DASH_DATETIME in UTC.
|
||||
expect(screen.getByTestId('log-details-header-timestamp')).toHaveTextContent(
|
||||
'Jan 15, 2024 ⎯ 09:45:30',
|
||||
);
|
||||
});
|
||||
|
||||
it('copies the log link from the ⋯ menu', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
renderDrawer();
|
||||
|
||||
await user.click(screen.getByTestId('log-details-header-menu'));
|
||||
await user.click(await screen.findByText('Copy link to log'));
|
||||
|
||||
expect(toast.success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('copies the log from the ⋯ menu', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
renderDrawer();
|
||||
|
||||
await user.click(screen.getByTestId('log-details-header-menu'));
|
||||
await user.click(await screen.findByText('Copy log'));
|
||||
|
||||
expect(toast.success).toHaveBeenCalledWith('Copied to clipboard', {
|
||||
position: 'bottom-right',
|
||||
});
|
||||
});
|
||||
|
||||
it('shows "Open in Explorer" when a handleOpenInExplorer handler is provided', () => {
|
||||
renderDrawer({ handleOpenInExplorer: jest.fn() });
|
||||
|
||||
expect(screen.getByText('Open in Explorer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides "Open in Explorer" when no handleOpenInExplorer handler is provided', () => {
|
||||
renderDrawer();
|
||||
|
||||
expect(screen.queryByText('Open in Explorer')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Highlights for fields present on the log, omitting absent ones', () => {
|
||||
const logWithMeta = {
|
||||
...mockLog,
|
||||
severity_text: 'ERROR',
|
||||
trace_id: 'trace-abc',
|
||||
resources_string: {
|
||||
'service.name': 'checkout',
|
||||
'deployment.environment': 'production',
|
||||
},
|
||||
} as unknown as ILog;
|
||||
|
||||
renderDrawer({ log: logWithMeta });
|
||||
|
||||
const highlights = screen.getByTestId('log-details-highlights');
|
||||
expect(highlights).toHaveTextContent('SEVERITY');
|
||||
expect(highlights).toHaveTextContent('ERROR');
|
||||
expect(highlights).toHaveTextContent('SERVICE');
|
||||
expect(highlights).toHaveTextContent('checkout');
|
||||
expect(highlights).toHaveTextContent('ENVIRONMENT');
|
||||
expect(highlights).toHaveTextContent('production');
|
||||
expect(highlights).toHaveTextContent('TRACE ID');
|
||||
// Absent fields are omitted (no namespace / span id on this log).
|
||||
expect(highlights).not.toHaveTextContent('NAMESPACE');
|
||||
expect(highlights).not.toHaveTextContent('SPAN ID');
|
||||
});
|
||||
|
||||
it('links the trace id highlight to the trace detail in a new tab', () => {
|
||||
const logWithTrace = {
|
||||
...mockLog,
|
||||
trace_id: 'trace-abc',
|
||||
} as unknown as ILog;
|
||||
|
||||
renderDrawer({ log: logWithTrace });
|
||||
|
||||
const link = screen.getByRole('link', { name: 'trace-abc' });
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
expect(link.getAttribute('href')).toContain('/trace/trace-abc');
|
||||
});
|
||||
|
||||
it('navigates to the next / previous log with the Down / Up arrow keys', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const logs = [makeLog('log-0'), makeLog('log-1'), makeLog('log-2')];
|
||||
const onNavigateLog = jest.fn();
|
||||
const onScrollToLog = jest.fn();
|
||||
|
||||
// Active log is the middle one so both directions are available.
|
||||
renderDrawer({ log: logs[1], logs, onNavigateLog, onScrollToLog });
|
||||
|
||||
await user.keyboard('{ArrowDown}');
|
||||
expect(onNavigateLog).toHaveBeenLastCalledWith(logs[2]);
|
||||
expect(onScrollToLog).toHaveBeenLastCalledWith('log-2');
|
||||
|
||||
await user.keyboard('{ArrowUp}');
|
||||
expect(onNavigateLog).toHaveBeenLastCalledWith(logs[0]);
|
||||
expect(onScrollToLog).toHaveBeenLastCalledWith('log-0');
|
||||
});
|
||||
|
||||
it('does not navigate past the first log on ArrowUp', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const logs = [makeLog('log-0'), makeLog('log-1')];
|
||||
const onNavigateLog = jest.fn();
|
||||
|
||||
renderDrawer({ log: logs[0], logs, onNavigateLog });
|
||||
|
||||
await user.keyboard('{ArrowUp}');
|
||||
expect(onNavigateLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('navigates via the header up / down buttons and disables them at boundaries', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const logs = [makeLog('log-0'), makeLog('log-1')];
|
||||
const onNavigateLog = jest.fn();
|
||||
|
||||
// Active log is the first one.
|
||||
renderDrawer({ log: logs[0], logs, onNavigateLog });
|
||||
|
||||
expect(screen.getByTestId('log-details-header-prev')).toBeDisabled();
|
||||
expect(screen.getByTestId('log-details-header-next')).toBeEnabled();
|
||||
|
||||
await user.click(screen.getByTestId('log-details-header-next'));
|
||||
expect(onNavigateLog).toHaveBeenLastCalledWith(logs[1]);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,10 @@
|
||||
import getLocalStorage from 'api/browser/localstorage/get';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
// Temp feature flag before actual roll-out
|
||||
export const isLogDetailsV2 =
|
||||
getLocalStorage(LOCALSTORAGE.LOG_DETAILS_V2) === 'true';
|
||||
|
||||
export const VIEW_TYPES = {
|
||||
OVERVIEW: 'OVERVIEW',
|
||||
JSON: 'JSON',
|
||||
|
||||
@@ -8,7 +8,9 @@ import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||
import LogStateIndicator, {
|
||||
LogType,
|
||||
} from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
||||
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
@@ -23,6 +25,7 @@ import {
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -48,8 +51,11 @@ import { ILogBody } from 'types/api/logs/log';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
|
||||
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
||||
import { isLogDetailsV2, RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
||||
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
|
||||
import LogDetailsHeader from './LogDetailsHeader/LogDetailsHeader';
|
||||
import { useLogNavigation } from './LogDetailsHeader/useLogNavigation';
|
||||
import LogHighlights from './LogHighlights/LogHighlights';
|
||||
|
||||
import './LogDetails.styles.scss';
|
||||
|
||||
@@ -96,7 +102,8 @@ function LogDetailInner({
|
||||
target.closest('[data-log-detail-ignore="true"]') ||
|
||||
target.closest('.cm-tooltip-autocomplete') ||
|
||||
target.closest('.drawer-popover') ||
|
||||
target.closest('.query-status-popover')
|
||||
target.closest('.query-status-popover') ||
|
||||
target.closest('[data-radix-popper-content-wrapper]')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -112,49 +119,30 @@ function LogDetailInner({
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
// Keyboard navigation - handle up/down arrow keys
|
||||
// Only listen when in OVERVIEW tab
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const { goToPrev, goToNext, isPrevDisabled, isNextDisabled } =
|
||||
useLogNavigation({
|
||||
logs,
|
||||
activeLogId: log.id,
|
||||
onNavigateLog,
|
||||
onScrollToLog,
|
||||
});
|
||||
|
||||
// Keyboard navigation - handle up/down arrow keys. Only listen in the OVERVIEW
|
||||
// tab so we don't hijack arrow keys from the JSON editor / context view.
|
||||
useEffect(() => {
|
||||
if (
|
||||
!logs ||
|
||||
!onNavigateLog ||
|
||||
logs.length === 0 ||
|
||||
selectedView !== VIEW_TYPES.OVERVIEW
|
||||
) {
|
||||
return;
|
||||
if (selectedView !== VIEW_TYPES.OVERVIEW) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent): void => {
|
||||
const currentIndex = logs.findIndex((l) => l.id === log.id);
|
||||
if (currentIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Navigate to previous log
|
||||
if (currentIndex > 0) {
|
||||
const prevLog = logs[currentIndex - 1];
|
||||
onNavigateLog(prevLog);
|
||||
// Trigger scroll to the log element
|
||||
if (onScrollToLog) {
|
||||
onScrollToLog(prevLog.id);
|
||||
}
|
||||
}
|
||||
goToPrev();
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Navigate to next log
|
||||
if (currentIndex < logs.length - 1) {
|
||||
const nextLog = logs[currentIndex + 1];
|
||||
onNavigateLog(nextLog);
|
||||
// Trigger scroll to the log element
|
||||
if (onScrollToLog) {
|
||||
onScrollToLog(nextLog.id);
|
||||
}
|
||||
}
|
||||
goToNext();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,7 +150,7 @@ function LogDetailInner({
|
||||
return (): void => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [log.id, logs, onNavigateLog, onScrollToLog, selectedView]);
|
||||
}, [selectedView, goToPrev, goToNext]);
|
||||
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) {
|
||||
@@ -303,33 +291,6 @@ function LogDetailInner({
|
||||
};
|
||||
|
||||
const logType = log?.attributes_string?.log_level || LogType.INFO;
|
||||
const currentLogIndex = logs ? logs.findIndex((l) => l.id === log.id) : -1;
|
||||
const isPrevDisabled =
|
||||
!logs || !onNavigateLog || logs.length === 0 || currentLogIndex <= 0;
|
||||
const isNextDisabled =
|
||||
!logs ||
|
||||
!onNavigateLog ||
|
||||
logs.length === 0 ||
|
||||
currentLogIndex === logs.length - 1;
|
||||
|
||||
type HandleNavigateLogParams = {
|
||||
direction: 'next' | 'previous';
|
||||
};
|
||||
|
||||
const handleNavigateLog = ({ direction }: HandleNavigateLogParams): void => {
|
||||
if (!logs || !onNavigateLog || currentLogIndex === -1) {
|
||||
return;
|
||||
}
|
||||
if (direction === 'previous' && !isPrevDisabled) {
|
||||
const prevLog = logs[currentLogIndex - 1];
|
||||
onNavigateLog(prevLog);
|
||||
onScrollToLog?.(prevLog.id);
|
||||
} else if (direction === 'next' && !isNextDisabled) {
|
||||
const nextLog = logs[currentLogIndex + 1];
|
||||
onNavigateLog(nextLog);
|
||||
onScrollToLog?.(nextLog.id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
@@ -338,57 +299,69 @@ function LogDetailInner({
|
||||
maskClosable={false}
|
||||
getContainer={getContainer}
|
||||
title={
|
||||
<div className="log-detail-drawer__title" data-log-detail-ignore="true">
|
||||
<div className="log-detail-drawer__title-left">
|
||||
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
|
||||
<Typography.Text className="title">Log details</Typography.Text>
|
||||
</div>
|
||||
<div className="log-detail-drawer__title-right">
|
||||
<div className="log-arrows">
|
||||
<Tooltip
|
||||
title={isPrevDisabled ? '' : 'Move to previous log'}
|
||||
placement="top"
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<ChevronUp size={14} />}
|
||||
className="log-arrow-btn log-arrow-btn-up"
|
||||
disabled={isPrevDisabled}
|
||||
onClick={(): void => handleNavigateLog({ direction: 'previous' })}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={isNextDisabled ? '' : 'Move to next log'}
|
||||
placement="top"
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<ChevronDown size={14} />}
|
||||
className="log-arrow-btn log-arrow-btn-down"
|
||||
disabled={isNextDisabled}
|
||||
onClick={(): void => handleNavigateLog({ direction: 'next' })}
|
||||
/>
|
||||
</Tooltip>
|
||||
isLogDetailsV2 ? (
|
||||
<LogDetailsHeader
|
||||
log={log}
|
||||
onNavigatePrev={goToPrev}
|
||||
onNavigateNext={goToNext}
|
||||
isPrevDisabled={isPrevDisabled}
|
||||
isNextDisabled={isNextDisabled}
|
||||
showOpenInExplorer={!!handleOpenInExplorer}
|
||||
onOpenInExplorer={handleOpenInExplorer}
|
||||
/>
|
||||
) : (
|
||||
<div className="log-detail-drawer__title" data-log-detail-ignore="true">
|
||||
<div className="log-detail-drawer__title-left">
|
||||
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
|
||||
<Typography.Text className="title">Log details</Typography.Text>
|
||||
</div>
|
||||
{handleOpenInExplorer && (
|
||||
<div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<Compass size={16} />}
|
||||
className="open-in-explorer-btn"
|
||||
onClick={handleOpenInExplorer}
|
||||
<div className="log-detail-drawer__title-right">
|
||||
<div className="log-arrows">
|
||||
<Tooltip
|
||||
title={isPrevDisabled ? '' : 'Move to previous log'}
|
||||
placement="top"
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
Open in Explorer
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<ChevronUp size={14} />}
|
||||
className="log-arrow-btn log-arrow-btn-up"
|
||||
disabled={isPrevDisabled}
|
||||
onClick={goToPrev}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={isNextDisabled ? '' : 'Move to next log'}
|
||||
placement="top"
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<ChevronDown size={14} />}
|
||||
className="log-arrow-btn log-arrow-btn-down"
|
||||
disabled={isNextDisabled}
|
||||
onClick={goToNext}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{handleOpenInExplorer && (
|
||||
<div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<Compass size={16} />}
|
||||
className="open-in-explorer-btn"
|
||||
onClick={handleOpenInExplorer}
|
||||
>
|
||||
Open in Explorer
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
placement="right"
|
||||
onClose={drawerCloseHandler}
|
||||
@@ -407,7 +380,15 @@ function LogDetailInner({
|
||||
data-testid="log-detail-drawer"
|
||||
>
|
||||
<div className="log-detail-drawer__log">
|
||||
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
|
||||
{isLogDetailsV2 ? (
|
||||
<LogStateIndicator
|
||||
severityText={log.severity_text}
|
||||
severityNumber={log.severity_number}
|
||||
fontSize={options?.fontSize ?? FontSize.MEDIUM}
|
||||
/>
|
||||
) : (
|
||||
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
|
||||
)}
|
||||
<Tooltip
|
||||
title={removeEscapeCharacters(logBody)}
|
||||
placement="left"
|
||||
@@ -419,6 +400,8 @@ function LogDetailInner({
|
||||
<div className="log-overflow-shadow"> </div>
|
||||
</div>
|
||||
|
||||
{isLogDetailsV2 && <LogHighlights log={log} />}
|
||||
|
||||
<div className="tabs-and-search">
|
||||
<ToggleGroupSimple
|
||||
type="single"
|
||||
@@ -483,22 +466,25 @@ function LogDetailInner({
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip
|
||||
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
|
||||
placement="topLeft"
|
||||
aria-label={
|
||||
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
|
||||
}
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
prefix={<Copy size={12} />}
|
||||
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
|
||||
/>
|
||||
</Tooltip>
|
||||
{/* V2 moves copy actions into the header ⋯ menu */}
|
||||
{!isLogDetailsV2 && (
|
||||
<Tooltip
|
||||
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
|
||||
placement="topLeft"
|
||||
aria-label={
|
||||
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
|
||||
}
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
prefix={<Copy size={12} />}
|
||||
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isFilterVisible && contextQuery?.builder.queryData[0] && (
|
||||
|
||||
@@ -183,15 +183,14 @@ function QuerySearch({
|
||||
isProgrammaticChangeRef.current = true;
|
||||
}
|
||||
|
||||
const changes = view.state.changes({
|
||||
from: 0,
|
||||
to: currentValue.length,
|
||||
insert: value,
|
||||
});
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: currentValue.length,
|
||||
insert: value,
|
||||
},
|
||||
selection: {
|
||||
anchor: value.length,
|
||||
},
|
||||
changes,
|
||||
selection: { anchor: changes.newLength },
|
||||
});
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -301,6 +301,66 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
dispatchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does not crash when the expression contains CRLF line breaks (issue #5869)', async () => {
|
||||
const dispatchSpy = jest.spyOn(EditorView.prototype, 'dispatch');
|
||||
const onChange = jest.fn() as jest.MockedFunction<(v: string) => void>;
|
||||
const initialExpression = "service.name = 'frontend'";
|
||||
// Filtering on a multi-line log value (CRLF) used to throw
|
||||
// "RangeError: Selection points outside of document".
|
||||
const crlfExpression = "body CONTAINS 'line1\r\nline2\r\nline3'";
|
||||
|
||||
const baseQueryData = {
|
||||
...initialQueriesMap.logs.builder.queryData[0],
|
||||
filter: { expression: initialExpression },
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<QuerySearch
|
||||
onChange={onChange}
|
||||
queryData={baseQueryData}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const editorContent = document.querySelector(
|
||||
CM_EDITOR_SELECTOR,
|
||||
) as HTMLElement;
|
||||
expect(editorContent.textContent || '').toBe(initialExpression);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
rerender(
|
||||
<QuerySearch
|
||||
onChange={onChange}
|
||||
queryData={{ ...baseQueryData, filter: { expression: crlfExpression } }}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>,
|
||||
);
|
||||
|
||||
// The programmatic replace dispatched without throwing, and the selection anchor
|
||||
// stayed within the CRLF-normalized document (the bug set it past the end).
|
||||
await waitFor(() => {
|
||||
const spec = dispatchSpy.mock.calls
|
||||
.map(
|
||||
(call) =>
|
||||
call[0] as {
|
||||
selection?: { anchor?: number };
|
||||
changes?: { newLength?: number };
|
||||
},
|
||||
)
|
||||
.find((s) => s?.selection?.anchor != null && s?.changes?.newLength != null);
|
||||
expect(spec).toBeDefined();
|
||||
expect(spec?.selection?.anchor).toBeLessThanOrEqual(
|
||||
spec?.changes?.newLength as number,
|
||||
);
|
||||
});
|
||||
|
||||
dispatchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('fetches key suggestions for metrics even without aggregateAttribute.key when showFilterSuggestionsWithoutMetric is true', async () => {
|
||||
const mockedGetKeys = getKeySuggestions as jest.MockedFunction<
|
||||
typeof getKeySuggestions
|
||||
|
||||
32
frontend/src/constants/generated/semconvFamilies.gen.ts
Normal file
32
frontend/src/constants/generated/semconvFamilies.gen.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// Code generated by scripts/semconv. DO NOT EDIT.
|
||||
|
||||
export type SemconvFamily = {
|
||||
readonly current: string;
|
||||
readonly old: readonly string[];
|
||||
readonly kind: 'attribute' | 'metric';
|
||||
readonly contexts: readonly string[];
|
||||
readonly signals: readonly string[];
|
||||
readonly applyToMetrics: readonly string[];
|
||||
readonly valueMap: Readonly<Record<string, string>>;
|
||||
};
|
||||
|
||||
export const SEMCONV_FAMILIES: readonly SemconvFamily[] = [
|
||||
{
|
||||
current: 'db.system.name',
|
||||
old: ['db.system'],
|
||||
kind: 'attribute',
|
||||
contexts: [],
|
||||
signals: [],
|
||||
applyToMetrics: [],
|
||||
valueMap: {},
|
||||
},
|
||||
{
|
||||
current: 'deployment.environment.name',
|
||||
old: ['deployment.environment'],
|
||||
kind: 'attribute',
|
||||
contexts: [],
|
||||
signals: [],
|
||||
applyToMetrics: [],
|
||||
valueMap: {},
|
||||
},
|
||||
] as const;
|
||||
@@ -13,6 +13,7 @@ export enum LOCALSTORAGE {
|
||||
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMN_SIZING = 'LOGS_LIST_COLUMN_SIZING',
|
||||
LOG_DETAILS_V2 = 'LOG_DETAILS_V2',
|
||||
LOGGED_IN_USER_NAME = 'LOGGED_IN_USER_NAME',
|
||||
LOGGED_IN_USER_EMAIL = 'LOGGED_IN_USER_EMAIL',
|
||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||
|
||||
@@ -92,6 +92,7 @@ const ROUTES = {
|
||||
AI_OBSERVABILITY_ATTRIBUTE_MAPPING: '/ai-observability/attribute-mapping',
|
||||
AI_OBSERVABILITY_BASE: '/ai-observability',
|
||||
AI_OBSERVABILITY_OVERVIEW: '/ai-observability/overview',
|
||||
AI_OBSERVABILITY_EXPLORER: '/ai-observability/explorer',
|
||||
AI_OBSERVABILITY_CONFIGURATION: '/ai-observability/configuration',
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -17,9 +17,12 @@ import { ChevronDown, Dot, PencilLine, Plug, Plus } from '@signozhq/icons';
|
||||
|
||||
import AzureCloudAccountSetupModal from '../../AzureCloudServices/AddNewAccount/CloudAccountSetupModal';
|
||||
import AzureAccountSettingsModal from '../../AzureCloudServices/EditAccount/AccountSettingsModal';
|
||||
import GcpCloudAccountSetupDrawer from '../../GoogleCloudPlatform/AddNewAccount/CloudAccountSetupDrawer';
|
||||
import GcpAccountSettingsDrawer from '../../GoogleCloudPlatform/EditAccount/AccountSettingsDrawer';
|
||||
import {
|
||||
mapAccountDtoToAwsCloudAccount,
|
||||
mapAccountDtoToAzureCloudAccount,
|
||||
mapAccountDtoToGcpCloudAccount,
|
||||
} from '../../mapCloudAccountFromDto';
|
||||
import AwsCloudAccountSetupModal from '../AddNewAccount/CloudAccountSetupModal';
|
||||
import AwsAccountSettingsModal from '../EditAccount/AccountSettingsModal';
|
||||
@@ -156,6 +159,18 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
|
||||
});
|
||||
}
|
||||
|
||||
if (type === IntegrationType.GCP_SERVICES) {
|
||||
raw.forEach((account) => {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
const mapped = mapAccountDtoToGcpCloudAccount(account);
|
||||
if (mapped) {
|
||||
mappedAccounts.push(mapped);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mappedAccounts;
|
||||
}, [listAccountsResponse, type]);
|
||||
|
||||
@@ -207,13 +222,23 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
|
||||
// log telemetry event when an account is viewed.
|
||||
useEffect(() => {
|
||||
if (activeAccount) {
|
||||
const { config } = activeAccount;
|
||||
let enabledRegions: string[];
|
||||
if ('regions' in config) {
|
||||
// AWS
|
||||
enabledRegions = config.regions;
|
||||
} else if ('resource_groups' in config) {
|
||||
// Azure
|
||||
enabledRegions = config.resource_groups;
|
||||
} else {
|
||||
// GCP
|
||||
enabledRegions = config.project_ids;
|
||||
}
|
||||
|
||||
logEvent(`${type} Integration: Account viewed`, {
|
||||
cloudAccountId: activeAccount?.cloud_account_id,
|
||||
status: activeAccount?.status,
|
||||
enabledRegions:
|
||||
'regions' in activeAccount.config
|
||||
? activeAccount.config.regions
|
||||
: activeAccount.config.resource_groups,
|
||||
enabledRegions,
|
||||
});
|
||||
}
|
||||
}, [activeAccount, type]);
|
||||
@@ -260,6 +285,11 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
|
||||
onClose={(): void => setIsIntegrationModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{type === IntegrationType.GCP_SERVICES && (
|
||||
<GcpCloudAccountSetupDrawer
|
||||
onClose={(): void => setIsIntegrationModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -281,6 +311,13 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
|
||||
setActiveAccount={setActiveAccount}
|
||||
/>
|
||||
)}
|
||||
{type === IntegrationType.GCP_SERVICES && (
|
||||
<GcpAccountSettingsDrawer
|
||||
onClose={(): void => setIsAccountSettingsModalOpen(false)}
|
||||
account={activeAccount}
|
||||
setActiveAccount={setActiveAccount}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -46,14 +46,34 @@ const EMPTY_FORM_VALUES: ServiceConfigFormValues = {
|
||||
s3BucketsByRegion: {},
|
||||
};
|
||||
|
||||
const GCP_METRICS_INFO_TOOLTIP =
|
||||
'These are suggested metrics for your OpenTelemetry Collector Configuration. The metrics you actually receive may vary based on the metrics listed in your collector config.';
|
||||
|
||||
function getIntegrationServiceConfig(
|
||||
type: IntegrationType,
|
||||
serviceDetailsData?: ServiceDetailsData,
|
||||
):
|
||||
| { logs?: { enabled?: boolean }; metrics?: { enabled?: boolean } }
|
||||
| undefined {
|
||||
const config = serviceDetailsData?.cloudIntegrationService?.config;
|
||||
|
||||
if (type === IntegrationType.AWS_SERVICES) {
|
||||
return config?.aws;
|
||||
}
|
||||
if (type === IntegrationType.GCP_SERVICES) {
|
||||
return config?.gcp;
|
||||
}
|
||||
return config?.azure;
|
||||
}
|
||||
|
||||
function getInitialFormValues(
|
||||
type: IntegrationType,
|
||||
serviceDetailsData?: ServiceDetailsData,
|
||||
): ServiceConfigFormValues {
|
||||
const integrationConfig =
|
||||
type === IntegrationType.AWS_SERVICES
|
||||
? serviceDetailsData?.cloudIntegrationService?.config?.aws
|
||||
: serviceDetailsData?.cloudIntegrationService?.config?.azure;
|
||||
const integrationConfig = getIntegrationServiceConfig(
|
||||
type,
|
||||
serviceDetailsData,
|
||||
);
|
||||
|
||||
return {
|
||||
logsEnabled: integrationConfig?.logs?.enabled || false,
|
||||
@@ -98,16 +118,21 @@ function getServiceConfigPayload({
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
azure: {
|
||||
logs: {
|
||||
enabled: isLogsSupported ? logsEnabled : false,
|
||||
},
|
||||
metrics: {
|
||||
enabled: isMetricsSupported ? metricsEnabled : false,
|
||||
},
|
||||
// Azure and GCP share the same simple logs/metrics enable-flag shape.
|
||||
const signalConfig = {
|
||||
logs: {
|
||||
enabled: isLogsSupported ? logsEnabled : false,
|
||||
},
|
||||
metrics: {
|
||||
enabled: isMetricsSupported ? metricsEnabled : false,
|
||||
},
|
||||
};
|
||||
|
||||
if (type === IntegrationType.GCP_SERVICES) {
|
||||
return { gcp: signalConfig };
|
||||
}
|
||||
|
||||
return { azure: signalConfig };
|
||||
}
|
||||
|
||||
function ServiceDetails({
|
||||
@@ -162,10 +187,10 @@ function ServiceDetails({
|
||||
? isAccountServiceLoading
|
||||
: isReadOnlyServiceLoading;
|
||||
|
||||
const integrationConfig =
|
||||
type === IntegrationType.AWS_SERVICES
|
||||
? serviceDetailsData?.cloudIntegrationService?.config?.aws
|
||||
: serviceDetailsData?.cloudIntegrationService?.config?.azure;
|
||||
const integrationConfig = getIntegrationServiceConfig(
|
||||
type,
|
||||
serviceDetailsData,
|
||||
);
|
||||
const isServiceEnabledInPersistedConfig =
|
||||
Boolean(integrationConfig?.logs?.enabled) ||
|
||||
Boolean(integrationConfig?.metrics?.enabled);
|
||||
@@ -477,6 +502,11 @@ function ServiceDetails({
|
||||
<CloudServiceDataCollected
|
||||
logsData={serviceDetailsData?.dataCollected?.logs || []}
|
||||
metricsData={serviceDetailsData?.dataCollected?.metrics || []}
|
||||
metricsInfoTooltip={
|
||||
type === IntegrationType.GCP_SERVICES
|
||||
? GCP_METRICS_INFO_TOOLTIP
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -36,8 +36,12 @@ function AccountSettingsModal({
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// `account.config` is the shared per-provider union (Azure | AWS | GCP).
|
||||
// Narrow to Azure by `resource_groups` (Azure-only) rather than
|
||||
// `deployment_region`, which GCP also has — so it no longer identifies
|
||||
// Azure uniquely.
|
||||
const azureConfig = useMemo(
|
||||
() => ('deployment_region' in account.config ? account.config : null),
|
||||
() => ('resource_groups' in account.config ? account.config : null),
|
||||
[account.config],
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
.setupDrawer {
|
||||
--dialog-header-padding: var(--spacing-10) var(--spacing-12);
|
||||
--dialog-footer-padding: var(--spacing-8) var(--spacing-12);
|
||||
|
||||
// Input and ComboboxSimple default to --border borders, inherited text and
|
||||
// --muted-foreground placeholders; the drawer wants the dimmer --l2-border with
|
||||
// brighter values and duller placeholders. Backgrounds are left alone — both
|
||||
// components default to transparent and every field sits on an --l2-background
|
||||
// surface already. Focus borders stay at their per-component defaults.
|
||||
--input-border-color: var(--l2-border);
|
||||
--input-hover-border-color: var(--l2-border);
|
||||
--input-foreground: var(--l1-foreground);
|
||||
--input-placeholder-color: var(--l3-foreground);
|
||||
--combobox-trigger-border-color: var(--l2-border);
|
||||
|
||||
// Bounded flex column so the header/footer stay put and only the body
|
||||
// scrolls when content overflows.
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
[data-slot='drawer-header'],
|
||||
[data-slot='drawer-footer'] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// The drawer body renders inside [data-slot='drawer-description'] — this is
|
||||
// the only region allowed to scroll.
|
||||
[data-slot='drawer-description'] {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-10);
|
||||
min-height: 0;
|
||||
padding: var(--spacing-10) var(--spacing-12);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
[data-slot='select-content'] {
|
||||
width: var(--radix-select-trigger-width);
|
||||
}
|
||||
|
||||
[data-slot='combobox-content'] {
|
||||
z-index: 5;
|
||||
background: var(--l1-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: var(--radius-2);
|
||||
}
|
||||
|
||||
// Selected region value: bright, like every other field's text.
|
||||
[data-slot='combobox-value'] {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
// Empty trigger (.regionEmpty, set from the RHF value): dull the placeholder text.
|
||||
.regionEmpty [data-slot='combobox-value'] {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: var(--periscope-font-size-medium);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
}
|
||||
|
||||
.footerContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.drawerSection {
|
||||
composes: drawerSection from './shared.module.scss';
|
||||
}
|
||||
|
||||
.mono {
|
||||
composes: mono from './shared.module.scss';
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fieldError {
|
||||
composes: fieldError from './shared.module.scss';
|
||||
}
|
||||
|
||||
.projectIdsSelect {
|
||||
:global(.ant-select-selector) {
|
||||
min-height: 36px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border) !important;
|
||||
}
|
||||
|
||||
&:hover :global(.ant-select-selector),
|
||||
&:global(.ant-select-focused) :global(.ant-select-selector) {
|
||||
border-color: var(--l2-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
// antd defaults to 14px; pin to 13px to line up with the Input/Combobox fields.
|
||||
:global(.ant-select-selection-placeholder),
|
||||
:global(.ant-select-selection-search-input),
|
||||
:global(.ant-select-selection-item) {
|
||||
font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
|
||||
:global(.ant-select-selection-placeholder) {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
:global(.ant-select-selection-search-input) {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
:global(.ant-select-selection-item) {
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: var(--radius-2);
|
||||
}
|
||||
|
||||
:global(.ant-select-selection-item-remove) {
|
||||
color: var(--l3-foreground);
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
import { ComboboxSimple } from '@signozhq/ui/combobox';
|
||||
import { DrawerWrapper } from '@signozhq/ui/drawer';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Select } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { GCP_REGIONS } from 'container/Integrations/constants';
|
||||
import { IntegrationModalProps } from 'container/Integrations/HeroSection/types';
|
||||
import { useCloudAccountSetupDrawer } from 'hooks/integration/gcp/useCloudAccountSetupDrawer';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import ConnectionSecretsFields from './ConnectionSecretsFields';
|
||||
import FieldLabel from './FieldLabel';
|
||||
import FlowSelector from './FlowSelector';
|
||||
import SetupGuideCallout from './SetupGuideCallout';
|
||||
import { GcpSetupFormValues, SetupFlow } from './types';
|
||||
|
||||
import styles from './CloudAccountSetupDrawer.module.scss';
|
||||
|
||||
const REGION_ITEMS = GCP_REGIONS.map((region) => ({
|
||||
value: region.value,
|
||||
label: `${region.label} (${region.value})`,
|
||||
}));
|
||||
|
||||
const DEFAULT_VALUES: GcpSetupFormValues = {
|
||||
accountName: '',
|
||||
deploymentProjectId: '',
|
||||
deploymentRegion: '',
|
||||
projectIds: [],
|
||||
sigNozApiUrl: '',
|
||||
sigNozApiKey: '',
|
||||
ingestionUrl: '',
|
||||
ingestionKey: '',
|
||||
};
|
||||
|
||||
function CloudAccountSetupDrawer({
|
||||
onClose,
|
||||
}: IntegrationModalProps): JSX.Element {
|
||||
const {
|
||||
isLoading,
|
||||
connectAccount,
|
||||
handleClose,
|
||||
connectionParams,
|
||||
isConnectionParamsLoading,
|
||||
submitError,
|
||||
clearSubmitError,
|
||||
} = useCloudAccountSetupDrawer({ onClose });
|
||||
|
||||
const { control, handleSubmit, setValue } = useForm<GcpSetupFormValues>({
|
||||
defaultValues: DEFAULT_VALUES,
|
||||
});
|
||||
|
||||
const [flow, setFlow] = useState<SetupFlow>('manual');
|
||||
|
||||
// Pre-fill the deployment/ingestion fields with the fetched credentials.
|
||||
useEffect(() => {
|
||||
if (!connectionParams) {
|
||||
return;
|
||||
}
|
||||
setValue('sigNozApiUrl', connectionParams.sigNozApiUrl);
|
||||
setValue('sigNozApiKey', connectionParams.sigNozApiKey);
|
||||
setValue('ingestionUrl', connectionParams.ingestionUrl);
|
||||
setValue('ingestionKey', connectionParams.ingestionKey);
|
||||
}, [connectionParams, setValue]);
|
||||
|
||||
const footer = (
|
||||
<div className={styles.footerContainer}>
|
||||
{submitError && (
|
||||
<Callout
|
||||
type="error"
|
||||
size="small"
|
||||
showIcon
|
||||
action="dismissible"
|
||||
onClick={clearSubmitError}
|
||||
title="Failed to connect GCP account"
|
||||
testId="gcp-connect-error"
|
||||
>
|
||||
{submitError}
|
||||
</Callout>
|
||||
)}
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={handleClose}
|
||||
testId="gcp-cancel-btn"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={handleSubmit(connectAccount)}
|
||||
loading={isLoading}
|
||||
disabled={isConnectionParamsLoading}
|
||||
testId="gcp-connect-account-btn"
|
||||
>
|
||||
Connect Account
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={true}
|
||||
className={styles.setupDrawer}
|
||||
onOpenChange={(open): void => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
direction="right"
|
||||
showCloseButton
|
||||
title="Connect Google Cloud Platform"
|
||||
width="base"
|
||||
footer={footer}
|
||||
drawerHeaderProps={{ className: styles.title }}
|
||||
>
|
||||
<FlowSelector value={flow} onChange={setFlow} />
|
||||
<SetupGuideCallout />
|
||||
<div className={styles.drawerSection}>
|
||||
<FieldLabel
|
||||
htmlFor="gcp-account-name-input"
|
||||
label="Account Name"
|
||||
tooltip="A label to identify this group of GCP projects (org ID, billing email, or any descriptive name)"
|
||||
required
|
||||
/>
|
||||
<Controller
|
||||
name="accountName"
|
||||
control={control}
|
||||
rules={{ required: 'Please enter an account name' }}
|
||||
render={({ field, fieldState }): JSX.Element => (
|
||||
<>
|
||||
<Input
|
||||
id="gcp-account-name-input"
|
||||
className={styles.fullWidth}
|
||||
placeholder="e.g. my-org or billing@company.com"
|
||||
value={field.value}
|
||||
onChange={(e): void => field.onChange(e.target.value)}
|
||||
testId="gcp-account-name-input"
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<Typography.Text
|
||||
as="span"
|
||||
size="small"
|
||||
role="alert"
|
||||
className={styles.fieldError}
|
||||
>
|
||||
{fieldState.error.message}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.drawerSection}>
|
||||
<FieldLabel
|
||||
htmlFor="gcp-deployment-project-id-input"
|
||||
label="Deployment Project ID"
|
||||
tooltip="The GCP project that hosts your OTel Collector deployment — often separate from the projects you actually monitor"
|
||||
required
|
||||
/>
|
||||
<Controller
|
||||
name="deploymentProjectId"
|
||||
control={control}
|
||||
rules={{ required: 'Please enter the deployment project ID' }}
|
||||
render={({ field, fieldState }): JSX.Element => (
|
||||
<>
|
||||
<Input
|
||||
id="gcp-deployment-project-id-input"
|
||||
className={cx(styles.fullWidth, styles.mono)}
|
||||
placeholder="e.g. my-deployment-project-123"
|
||||
value={field.value}
|
||||
onChange={(e): void => field.onChange(e.target.value)}
|
||||
testId="gcp-deployment-project-id-input"
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<Typography.Text
|
||||
as="span"
|
||||
size="small"
|
||||
role="alert"
|
||||
className={styles.fieldError}
|
||||
>
|
||||
{fieldState.error.message}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.drawerSection}>
|
||||
<FieldLabel
|
||||
htmlFor="gcp-deployment-region-select"
|
||||
label="Deployment Region"
|
||||
tooltip="The GCP region where your OTel Collector will be deployed"
|
||||
required
|
||||
/>
|
||||
<Controller
|
||||
name="deploymentRegion"
|
||||
control={control}
|
||||
rules={{ required: 'Please select a region' }}
|
||||
render={({ field, fieldState }): JSX.Element => (
|
||||
<>
|
||||
<ComboboxSimple
|
||||
id="gcp-deployment-region-select"
|
||||
className={cx(styles.fullWidth, {
|
||||
[styles.regionEmpty]: !field.value,
|
||||
})}
|
||||
items={REGION_ITEMS}
|
||||
value={field.value}
|
||||
onChange={(value): void => field.onChange(value as string)}
|
||||
placeholder="Select a region..."
|
||||
inputPlaceholder="Search regions…"
|
||||
withPortal={false}
|
||||
testId="gcp-deployment-region-select"
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<Typography.Text
|
||||
as="span"
|
||||
size="small"
|
||||
role="alert"
|
||||
className={styles.fieldError}
|
||||
>
|
||||
{fieldState.error.message}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.drawerSection}>
|
||||
<FieldLabel
|
||||
htmlFor="gcp-project-ids-select"
|
||||
label="Projects to Monitor"
|
||||
tooltip="Enter each GCP project ID then press Enter"
|
||||
required
|
||||
/>
|
||||
<Controller
|
||||
name="projectIds"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (value): true | string =>
|
||||
value.length > 0 || 'Please add at least one project ID',
|
||||
}}
|
||||
render={({ field, fieldState }): JSX.Element => (
|
||||
<>
|
||||
<Select
|
||||
id="gcp-project-ids-select"
|
||||
className={cx(styles.fullWidth, styles.projectIdsSelect)}
|
||||
mode="tags"
|
||||
value={field.value}
|
||||
onChange={(value): void => field.onChange(value)}
|
||||
placeholder="Add project IDs…"
|
||||
tokenSeparators={[',', ' ']}
|
||||
notFoundContent={null}
|
||||
suffixIcon={null}
|
||||
getPopupContainer={popupContainer}
|
||||
data-testid="gcp-project-ids-select"
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<Typography.Text
|
||||
as="span"
|
||||
size="small"
|
||||
role="alert"
|
||||
className={styles.fieldError}
|
||||
>
|
||||
{fieldState.error.message}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<ConnectionSecretsFields
|
||||
control={control}
|
||||
isLoading={isConnectionParamsLoading}
|
||||
connectionParams={connectionParams}
|
||||
/>
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default CloudAccountSetupDrawer;
|
||||
@@ -0,0 +1,71 @@
|
||||
.drawerSurface {
|
||||
composes: drawerSurface from './shared.module.scss';
|
||||
}
|
||||
|
||||
.drawerSurfaceHead {
|
||||
composes: drawerSurfaceHead from './shared.module.scss';
|
||||
}
|
||||
|
||||
.drawerSection {
|
||||
composes: drawerSection from './shared.module.scss';
|
||||
}
|
||||
|
||||
.headLabel {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.mono {
|
||||
composes: mono from './shared.module.scss';
|
||||
}
|
||||
|
||||
.fieldError {
|
||||
composes: fieldError from './shared.module.scss';
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.secretsBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-6);
|
||||
}
|
||||
|
||||
.skeletonLabel :global(.ant-skeleton-input) {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.skeletonInput :global(.ant-skeleton-input) {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.readonlyField {
|
||||
display: flex;
|
||||
gap: var(--spacing-2);
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
padding: 0 var(--spacing-2) 0 var(--spacing-4);
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: var(--radius-2);
|
||||
}
|
||||
|
||||
.readonlyValue {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
// Match the other fields' value text: 13px and the brighter --l1-foreground.
|
||||
font-size: var(--periscope-font-size-base);
|
||||
color: var(--l2-foreground);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { Lock } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Skeleton } from 'antd';
|
||||
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import cx from 'classnames';
|
||||
import CopyButton from 'periscope/components/CopyButton/CopyButton';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
|
||||
import FieldLabel from './FieldLabel';
|
||||
import { GcpSetupFormValues } from './types';
|
||||
import { SecretFieldType, validateSecretValue } from './validators';
|
||||
import styles from './ConnectionSecretsFields.module.scss';
|
||||
|
||||
type CredentialField = keyof CloudintegrationtypesCredentialsDTO;
|
||||
|
||||
interface FieldConfig {
|
||||
name: CredentialField;
|
||||
label: string;
|
||||
tooltip: string;
|
||||
placeholder: string;
|
||||
testId: string;
|
||||
type: SecretFieldType;
|
||||
}
|
||||
|
||||
const FIELDS: FieldConfig[] = [
|
||||
{
|
||||
name: 'sigNozApiUrl',
|
||||
label: 'SigNoz API URL',
|
||||
tooltip: 'Base URL of your SigNoz instance the collector reports to',
|
||||
placeholder: 'https://<tenant>.signoz.cloud',
|
||||
testId: 'gcp-signoz-api-url-input',
|
||||
type: 'url',
|
||||
},
|
||||
{
|
||||
name: 'sigNozApiKey',
|
||||
label: 'SigNoz API Key',
|
||||
tooltip: 'API key used to authenticate with your SigNoz instance',
|
||||
placeholder: 'Enter SigNoz API key',
|
||||
testId: 'gcp-signoz-api-key-input',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'ingestionUrl',
|
||||
label: 'Ingestion URL',
|
||||
tooltip: 'OTLP ingestion endpoint your OTel Collector sends telemetry to',
|
||||
placeholder: 'https://ingest.<region>.signoz.cloud',
|
||||
testId: 'gcp-ingestion-url-input',
|
||||
type: 'url',
|
||||
},
|
||||
{
|
||||
name: 'ingestionKey',
|
||||
label: 'Ingestion Key',
|
||||
tooltip: 'Ingestion key that authorizes telemetry sent to SigNoz',
|
||||
placeholder: 'Enter ingestion key',
|
||||
testId: 'gcp-ingestion-key-input',
|
||||
type: 'text',
|
||||
},
|
||||
];
|
||||
|
||||
interface ConnectionSecretsFieldsProps {
|
||||
control: Control<GcpSetupFormValues>;
|
||||
isLoading: boolean;
|
||||
connectionParams?: CloudintegrationtypesCredentialsDTO;
|
||||
}
|
||||
|
||||
function ConnectionSecretsFields({
|
||||
control,
|
||||
isLoading,
|
||||
connectionParams,
|
||||
}: ConnectionSecretsFieldsProps): JSX.Element {
|
||||
const hasMissingValue = FIELDS.some(
|
||||
(field) => !connectionParams?.[field.name],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.drawerSurface}>
|
||||
<div className={styles.drawerSurfaceHead}>
|
||||
<Typography.Text weight="bold" size="base">
|
||||
Deployment details & ingestion secrets
|
||||
</Typography.Text>
|
||||
{!hasMissingValue && (
|
||||
<div className={styles.headLabel}>
|
||||
<Lock size={12} />
|
||||
<Typography.Text as="span" size="small" className={styles.headLabel}>
|
||||
Auto-filled by SigNoz
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className={styles.secretsBody} data-testid="gcp-secrets-skeleton">
|
||||
{FIELDS.map((field) => (
|
||||
<div key={field.name} className={styles.drawerSection}>
|
||||
<Skeleton.Input active size="small" className={styles.skeletonLabel} />
|
||||
<Skeleton.Input active block className={styles.skeletonInput} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.secretsBody}>
|
||||
{FIELDS.map((field) => {
|
||||
// Backend-provided values are read-only — the user can't edit them, so
|
||||
// show a truncated value with a copy button. Missing values (enterprise)
|
||||
// stay editable inputs with no copy button.
|
||||
const providedValue = connectionParams?.[field.name];
|
||||
if (providedValue) {
|
||||
return (
|
||||
<div key={field.name} className={styles.drawerSection}>
|
||||
<FieldLabel
|
||||
htmlFor={field.testId}
|
||||
label={field.label}
|
||||
tooltip={field.tooltip}
|
||||
/>
|
||||
<div className={styles.readonlyField}>
|
||||
<Typography.Text
|
||||
as="span"
|
||||
id={field.testId}
|
||||
className={cx(styles.readonlyValue, styles.mono)}
|
||||
title={providedValue}
|
||||
testId={field.testId}
|
||||
>
|
||||
{providedValue}
|
||||
</Typography.Text>
|
||||
<CopyButton
|
||||
value={providedValue}
|
||||
size={12}
|
||||
ariaLabel={`Copy ${field.label}`}
|
||||
testId={`${field.testId}-copy`}
|
||||
onCopy={(): void => {
|
||||
toast.success(`${field.label} copied to clipboard`, {
|
||||
position: 'bottom-right',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={field.name} className={styles.drawerSection}>
|
||||
<FieldLabel
|
||||
htmlFor={field.testId}
|
||||
label={field.label}
|
||||
tooltip={field.tooltip}
|
||||
/>
|
||||
<Controller
|
||||
name={field.name}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (value): true | string =>
|
||||
validateSecretValue(field.label, field.type, value),
|
||||
}}
|
||||
render={({ field: rhfField, fieldState }): JSX.Element => (
|
||||
<>
|
||||
<Input
|
||||
id={field.testId}
|
||||
className={cx(styles.fullWidth, styles.mono)}
|
||||
placeholder={field.placeholder}
|
||||
value={rhfField.value}
|
||||
onChange={(e): void => rhfField.onChange(e.target.value)}
|
||||
testId={field.testId}
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<Typography.Text
|
||||
as="span"
|
||||
size="small"
|
||||
role="alert"
|
||||
className={styles.fieldError}
|
||||
>
|
||||
{fieldState.error.message}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ConnectionSecretsFields.defaultProps = {
|
||||
connectionParams: undefined,
|
||||
};
|
||||
|
||||
export default ConnectionSecretsFields;
|
||||
@@ -0,0 +1,22 @@
|
||||
.fieldLabel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.required {
|
||||
composes: required from './shared.module.scss';
|
||||
}
|
||||
|
||||
.infoTrigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--l3-foreground);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.tooltipContent {
|
||||
max-width: 240px;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Info } from '@signozhq/icons';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
|
||||
import styles from './FieldLabel.module.scss';
|
||||
|
||||
interface FieldLabelProps {
|
||||
htmlFor: string;
|
||||
label: string;
|
||||
tooltip: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
function FieldLabel({
|
||||
htmlFor,
|
||||
label,
|
||||
tooltip,
|
||||
required,
|
||||
}: FieldLabelProps): JSX.Element {
|
||||
return (
|
||||
<label className={styles.fieldLabel} htmlFor={htmlFor}>
|
||||
{label}
|
||||
|
||||
<TooltipSimple
|
||||
title={tooltip}
|
||||
side="top"
|
||||
tooltipContentProps={{ className: styles.tooltipContent }}
|
||||
>
|
||||
<span
|
||||
className={styles.infoTrigger}
|
||||
aria-label={`${label} help`}
|
||||
data-testid={`${htmlFor}-tooltip`}
|
||||
>
|
||||
<Info size={12} />
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
{required && (
|
||||
<span className={styles.required} aria-hidden="true">
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
FieldLabel.defaultProps = {
|
||||
required: false,
|
||||
};
|
||||
|
||||
export default FieldLabel;
|
||||
@@ -0,0 +1,84 @@
|
||||
.drawerSection {
|
||||
composes: drawerSection from './shared.module.scss';
|
||||
}
|
||||
|
||||
.drawerSurface {
|
||||
composes: drawerSurface from './shared.module.scss';
|
||||
}
|
||||
|
||||
.drawerSurfaceHead {
|
||||
composes: drawerSurfaceHead from './shared.module.scss';
|
||||
}
|
||||
|
||||
.flowRadioGroup {
|
||||
--radio-group-item-border-color: var(--l2-border);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
width: 100%;
|
||||
|
||||
.flowRadio {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: var(--spacing-5);
|
||||
width: 100%;
|
||||
padding: var(--spacing-5) var(--spacing-6);
|
||||
margin: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-2);
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.12s ease,
|
||||
border-color 0.12s ease;
|
||||
|
||||
> button[role='radio'] {
|
||||
flex: 0 0 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
> label {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: block;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&.flowRadioManual:has(button[data-state='checked']) {
|
||||
background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
|
||||
border-color: color-mix(in srgb, var(--accent-primary) 30%, transparent);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background-hover);
|
||||
}
|
||||
|
||||
&:has(button[disabled]) {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flowRadioTitle {
|
||||
display: flex;
|
||||
gap: var(--spacing-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flowRadioDesc {
|
||||
margin-top: var(--spacing-2);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
|
||||
import { SetupFlow } from './types';
|
||||
import styles from './FlowSelector.module.scss';
|
||||
|
||||
interface FlowSelectorProps {
|
||||
value: SetupFlow;
|
||||
onChange: (flow: SetupFlow) => void;
|
||||
}
|
||||
|
||||
function FlowSelector({ value, onChange }: FlowSelectorProps): JSX.Element {
|
||||
return (
|
||||
<div className={cx(styles.drawerSection, styles.drawerSurface)}>
|
||||
<div className={styles.drawerSurfaceHead}>
|
||||
<Typography.Text weight="bold" size="base">
|
||||
Connection method
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<RadioGroup
|
||||
value={value}
|
||||
onChange={(next): void => onChange(next as SetupFlow)}
|
||||
className={styles.flowRadioGroup}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value="manual"
|
||||
containerClassName={cx(styles.flowRadio, styles.flowRadioManual)}
|
||||
testId="gcp-flow-manual"
|
||||
>
|
||||
<div className={styles.flowRadioTitle}>
|
||||
<Typography.Text weight="semibold" size="base">
|
||||
Connect Manually
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text
|
||||
as="p"
|
||||
size="small"
|
||||
color="muted"
|
||||
className={styles.flowRadioDesc}
|
||||
>
|
||||
Deploy your own OTel Collector.
|
||||
</Typography.Text>
|
||||
</RadioGroupItem>
|
||||
|
||||
<RadioGroupItem
|
||||
value="agent"
|
||||
containerClassName={styles.flowRadio}
|
||||
testId="gcp-flow-agent"
|
||||
disabled
|
||||
>
|
||||
<div className={styles.flowRadioTitle}>
|
||||
<Typography.Text weight="semibold" size="base">
|
||||
Connect via Agent
|
||||
</Typography.Text>
|
||||
<Badge color="robin" variant="default">
|
||||
Soon
|
||||
</Badge>
|
||||
</div>
|
||||
<Typography.Text
|
||||
as="p"
|
||||
size="small"
|
||||
color="muted"
|
||||
className={styles.flowRadioDesc}
|
||||
>
|
||||
SigNoz deploys and manages the collector for you.
|
||||
</Typography.Text>
|
||||
</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FlowSelector;
|
||||
@@ -0,0 +1,13 @@
|
||||
.guideLink {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
color: var(--callout-primary-title);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ArrowUpRight, KeyRound } from '@signozhq/icons';
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from './SetupGuideCallout.module.scss';
|
||||
|
||||
const GCP_INTEGRATION_DOCS_URL =
|
||||
'https://signoz.io/docs/integrations/gcp/gcp-integration/';
|
||||
|
||||
function SetupGuideCallout(): JSX.Element {
|
||||
return (
|
||||
<Callout icon={<KeyRound />} testId="gcp-setup-guide-callout">
|
||||
<Typography.Text as="span" size="base">
|
||||
Please go through our GCP integration guide, which covers all prerequisites
|
||||
— service account, IAM roles, and resource setup.
|
||||
</Typography.Text>
|
||||
<a
|
||||
className={styles.guideLink}
|
||||
href={GCP_INTEGRATION_DOCS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-testid="gcp-setup-guide-link"
|
||||
>
|
||||
GCP integration guide
|
||||
<ArrowUpRight size={12} />
|
||||
</a>
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
export default SetupGuideCallout;
|
||||
@@ -0,0 +1,36 @@
|
||||
.drawerSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.drawerSection > label {
|
||||
font-size: var(--periscope-font-size-normal);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--accent-cherry);
|
||||
}
|
||||
|
||||
.fieldError {
|
||||
font-size: var(--periscope-font-size-small);
|
||||
color: var(--accent-cherry);
|
||||
}
|
||||
|
||||
.drawerSurface {
|
||||
padding: var(--spacing-7);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
.drawerSurfaceHead {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-5);
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export type SetupFlow = 'manual' | 'agent';
|
||||
|
||||
export interface GcpSetupFormValues {
|
||||
accountName: string;
|
||||
deploymentProjectId: string;
|
||||
deploymentRegion: string;
|
||||
projectIds: string[];
|
||||
sigNozApiUrl: string;
|
||||
sigNozApiKey: string;
|
||||
ingestionUrl: string;
|
||||
ingestionKey: string;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export type SecretFieldType = 'url' | 'text';
|
||||
|
||||
export function isValidUrl(value: string): boolean {
|
||||
try {
|
||||
return Boolean(new URL(value));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function validateSecretValue(
|
||||
label: string,
|
||||
type: SecretFieldType,
|
||||
value: string | undefined,
|
||||
): true | string {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
return `Please enter the ${label}`;
|
||||
}
|
||||
if (type === 'url' && !isValidUrl(trimmed)) {
|
||||
return `Please enter a valid URL for ${label}`;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 17px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.connectedAccountDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.connectedAccountDetailsTitle {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-20);
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.accountId {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
|
||||
.accountIdValue {
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
|
||||
.regionSelector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.regionSelectorTitle {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-20);
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.regionSelectorDescription {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-5);
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Save } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DrawerWrapper } from '@signozhq/ui/drawer';
|
||||
import { Form, Select } from 'antd';
|
||||
import { invalidateListAccounts } from 'api/generated/services/cloudintegration';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { CloudAccount } from 'container/Integrations/types';
|
||||
import { useAccountSettingsDrawer } from 'hooks/integration/gcp/useAccountSettingsDrawer';
|
||||
|
||||
import RemoveIntegrationAccount from '../../RemoveAccount/RemoveIntegrationAccount';
|
||||
|
||||
import styles from './AccountSettingsDrawer.module.scss';
|
||||
|
||||
interface AccountSettingsDrawerProps {
|
||||
onClose: () => void;
|
||||
account: CloudAccount;
|
||||
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
|
||||
}
|
||||
|
||||
function AccountSettingsDrawer({
|
||||
onClose,
|
||||
account,
|
||||
setActiveAccount,
|
||||
}: AccountSettingsDrawerProps): JSX.Element {
|
||||
const {
|
||||
form,
|
||||
isLoading,
|
||||
projectIds,
|
||||
isSaveDisabled,
|
||||
setProjectIds,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
} = useAccountSettingsDrawer({ onClose, account, setActiveAccount });
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const gcpConfig = useMemo(
|
||||
() => ('project_ids' in account.config ? account.config : null),
|
||||
[account.config],
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={true}
|
||||
title="Account Settings"
|
||||
direction="right"
|
||||
showCloseButton
|
||||
onOpenChange={(open): void => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
width="wide"
|
||||
footer={
|
||||
<div className={styles.footer}>
|
||||
<RemoveIntegrationAccount
|
||||
accountId={account?.id}
|
||||
onRemoveIntegrationAccountSuccess={(): void => {
|
||||
void invalidateListAccounts(queryClient, {
|
||||
cloudProvider: INTEGRATION_TYPES.GCP,
|
||||
});
|
||||
setActiveAccount(null);
|
||||
handleClose();
|
||||
}}
|
||||
cloudProvider={INTEGRATION_TYPES.GCP}
|
||||
/>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
disabled={isSaveDisabled}
|
||||
onClick={handleSubmit}
|
||||
loading={isLoading}
|
||||
prefix={<Save size={14} />}
|
||||
data-testid="gcp-update-account-btn"
|
||||
>
|
||||
Update Changes
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
projectIds: gcpConfig?.project_ids || [],
|
||||
}}
|
||||
>
|
||||
<div className={styles.body}>
|
||||
<div className={styles.connectedAccountDetails}>
|
||||
<div className={styles.connectedAccountDetailsTitle}>
|
||||
Connected Account details
|
||||
</div>
|
||||
<div className={styles.accountId}>
|
||||
Account Name:{' '}
|
||||
<span className={styles.accountIdValue}>
|
||||
{account?.providerAccountId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{gcpConfig?.deployment_project_id && (
|
||||
<div className={styles.regionSelector}>
|
||||
<div className={styles.regionSelectorTitle}>Deployment project ID</div>
|
||||
<div className={styles.regionSelectorDescription}>
|
||||
{gcpConfig.deployment_project_id}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{gcpConfig?.deployment_region && (
|
||||
<div className={styles.regionSelector}>
|
||||
<div className={styles.regionSelectorTitle}>Deployment region</div>
|
||||
<div className={styles.regionSelectorDescription}>
|
||||
{gcpConfig.deployment_region}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.regionSelector}>
|
||||
<div className={styles.regionSelectorTitle}>Projects to monitor</div>
|
||||
<div className={styles.regionSelectorDescription}>
|
||||
Update the GCP project IDs that should be monitored.
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
name="projectIds"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
type: 'array',
|
||||
min: 1,
|
||||
message: 'Please add at least one project ID',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
value={projectIds}
|
||||
tokenSeparators={[',']}
|
||||
onChange={(values): void => {
|
||||
setProjectIds(values);
|
||||
form.setFieldValue('projectIds', values);
|
||||
}}
|
||||
data-testid="gcp-edit-project-ids-select"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountSettingsDrawer;
|
||||
@@ -0,0 +1,230 @@
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest, RestRequest } from 'msw';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
import AccountSettingsDrawer from '../EditAccount/AccountSettingsDrawer';
|
||||
import {
|
||||
GCP_ACCOUNT_ID,
|
||||
GCP_ACCOUNT_URL,
|
||||
GCP_ACCOUNTS_URL,
|
||||
gcpAccount,
|
||||
gcpAccountConfig,
|
||||
listAccountsResponse,
|
||||
} from './mockData';
|
||||
|
||||
// `useAccountSettingsDrawer` imports logEvent by relative path, which the
|
||||
// jest.config moduleNameMapper (keyed on the `api/common/logEvent` alias) does
|
||||
// not intercept — so mock the resolved module directly.
|
||||
jest.mock('../../../../../api/common/logEvent', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const onClose = jest.fn();
|
||||
const setActiveAccount = jest.fn();
|
||||
|
||||
const renderDrawer = (): void => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<TooltipProvider>
|
||||
<AccountSettingsDrawer
|
||||
onClose={onClose}
|
||||
account={gcpAccount}
|
||||
setActiveAccount={setActiveAccount}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
/** The antd tags Select renders its text input inside the testId wrapper. */
|
||||
const getProjectIdsInput = (): HTMLElement =>
|
||||
within(screen.getByTestId('gcp-edit-project-ids-select')).getByRole(
|
||||
'combobox',
|
||||
);
|
||||
|
||||
/** Each selected tag carries an antd "close" icon that removes it. */
|
||||
const getProjectIdTagRemoveButtons = (): HTMLElement[] =>
|
||||
within(screen.getByTestId('gcp-edit-project-ids-select')).queryAllByLabelText(
|
||||
'close',
|
||||
);
|
||||
|
||||
describe('GCP AccountSettingsDrawer', () => {
|
||||
let updatePayload: Record<string, unknown> | null;
|
||||
|
||||
beforeEach(() => {
|
||||
updatePayload = null;
|
||||
|
||||
server.use(
|
||||
rest.get(GCP_ACCOUNTS_URL, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(listAccountsResponse)),
|
||||
),
|
||||
rest.put(GCP_ACCOUNT_URL, async (req: RestRequest, res, ctx) => {
|
||||
updatePayload = await req.json();
|
||||
return res(ctx.status(204));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the connected account details and existing project IDs', () => {
|
||||
renderDrawer();
|
||||
|
||||
expect(screen.getByText(gcpAccount.providerAccountId)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(gcpAccountConfig.deployment_project_id),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(gcpAccountConfig.deployment_region),
|
||||
).toBeInTheDocument();
|
||||
|
||||
gcpAccountConfig.project_ids.forEach((projectId) => {
|
||||
expect(screen.getByTitle(projectId)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps save disabled until the project IDs actually change', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderDrawer();
|
||||
|
||||
expect(screen.getByTestId('gcp-update-account-btn')).toBeDisabled();
|
||||
|
||||
await user.type(getProjectIdsInput(), 'project-c,');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('gcp-update-account-btn')).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('sends the updated project IDs while preserving the immutable deployment fields', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderDrawer();
|
||||
|
||||
await user.type(getProjectIdsInput(), 'project-c,');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('gcp-update-account-btn')).toBeEnabled();
|
||||
});
|
||||
await user.click(screen.getByTestId('gcp-update-account-btn'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updatePayload).not.toBeNull();
|
||||
});
|
||||
|
||||
expect(updatePayload).toStrictEqual({
|
||||
config: {
|
||||
gcp: {
|
||||
deploymentRegion: gcpAccountConfig.deployment_region,
|
||||
deploymentProjectId: gcpAccountConfig.deployment_project_id,
|
||||
projectIds: ['project-a', 'project-b', 'project-c'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setActiveAccount).toHaveBeenCalledWith({
|
||||
...gcpAccount,
|
||||
config: {
|
||||
deployment_region: gcpAccountConfig.deployment_region,
|
||||
deployment_project_id: gcpAccountConfig.deployment_project_id,
|
||||
project_ids: ['project-a', 'project-b', 'project-c'],
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
'Account settings updated successfully',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('blocks the update and shows a validation error when every project ID is removed', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderDrawer();
|
||||
|
||||
// Strip every tag via its remove icon; the list shrinks as we go.
|
||||
while (getProjectIdTagRemoveButtons().length > 0) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await user.click(getProjectIdTagRemoveButtons()[0]);
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('gcp-update-account-btn')).toBeEnabled();
|
||||
});
|
||||
await user.click(screen.getByTestId('gcp-update-account-btn'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('Please add at least one project ID'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(updatePayload).toBeNull();
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('surfaces a toast and keeps the drawer open when the update fails', async () => {
|
||||
server.use(
|
||||
rest.put(GCP_ACCOUNT_URL, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(500),
|
||||
ctx.json({ status: 'error', error: { message: 'update failed' } }),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderDrawer();
|
||||
|
||||
await user.type(getProjectIdsInput(), 'project-c,');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('gcp-update-account-btn')).toBeEnabled();
|
||||
});
|
||||
await user.click(screen.getByTestId('gcp-update-account-btn'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
'Failed to update account settings',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
expect(setActiveAccount).not.toHaveBeenCalled();
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('disconnects the account with the GCP-specific confirmation copy', async () => {
|
||||
let disconnectedId: string | null = null;
|
||||
server.use(
|
||||
rest.delete(`${GCP_ACCOUNTS_URL}/:id`, (req, res, ctx) => {
|
||||
disconnectedId = req.params.id as string;
|
||||
return res(ctx.status(204));
|
||||
}),
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderDrawer();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /disconnect/i }));
|
||||
|
||||
await expect(
|
||||
screen.findByText(/manually tear down/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /remove account/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(disconnectedId).toBe(GCP_ACCOUNT_ID);
|
||||
});
|
||||
expect(setActiveAccount).toHaveBeenCalledWith(null);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest, RestRequest } from 'msw';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
import CloudAccountSetupDrawer from '../AddNewAccount/CloudAccountSetupDrawer';
|
||||
import {
|
||||
checkInResponse,
|
||||
CLOUD_INTEGRATION_ID,
|
||||
connectionCredentials,
|
||||
connectionCredentialsResponse,
|
||||
createAccountResponse,
|
||||
GCP_ACCOUNTS_URL,
|
||||
GCP_CHECK_IN_URL,
|
||||
GCP_CREDENTIALS_URL,
|
||||
} from './mockData';
|
||||
|
||||
// `useCloudAccountSetupDrawer` imports logEvent by relative path, which the
|
||||
// jest.config moduleNameMapper (keyed on the `api/common/logEvent` alias) does
|
||||
// not intercept — so mock the resolved module directly.
|
||||
jest.mock('../../../../../api/common/logEvent', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const onClose = jest.fn();
|
||||
|
||||
const renderDrawer = (): void => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<TooltipProvider>
|
||||
<CloudAccountSetupDrawer onClose={onClose} />
|
||||
</TooltipProvider>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
describe('GCP CloudAccountSetupDrawer', () => {
|
||||
let createAccountPayload: Record<string, unknown> | null;
|
||||
let checkInPayload: Record<string, unknown> | null;
|
||||
|
||||
beforeEach(() => {
|
||||
createAccountPayload = null;
|
||||
checkInPayload = null;
|
||||
|
||||
server.use(
|
||||
rest.get(GCP_CREDENTIALS_URL, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(connectionCredentialsResponse)),
|
||||
),
|
||||
// check_in is registered first — it is a more specific path than
|
||||
// /accounts and msw matches handlers in registration order.
|
||||
rest.post(GCP_CHECK_IN_URL, async (req: RestRequest, res, ctx) => {
|
||||
checkInPayload = await req.json();
|
||||
return res(ctx.status(200), ctx.json(checkInResponse));
|
||||
}),
|
||||
rest.post(GCP_ACCOUNTS_URL, async (req: RestRequest, res, ctx) => {
|
||||
createAccountPayload = await req.json();
|
||||
return res(ctx.status(201), ctx.json(createAccountResponse));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders SigNoz-provided credentials as read-only fields', async () => {
|
||||
renderDrawer();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('gcp-signoz-api-url-input')).toHaveTextContent(
|
||||
connectionCredentials.sigNozApiUrl,
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('gcp-signoz-api-key-input')).toHaveTextContent(
|
||||
connectionCredentials.sigNozApiKey,
|
||||
);
|
||||
expect(screen.getByTestId('gcp-ingestion-url-input')).toHaveTextContent(
|
||||
connectionCredentials.ingestionUrl,
|
||||
);
|
||||
expect(screen.getByTestId('gcp-ingestion-key-input')).toHaveTextContent(
|
||||
connectionCredentials.ingestionKey,
|
||||
);
|
||||
expect(screen.getByText('Auto-filled by SigNoz')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('blocks submission and surfaces validation errors when the form is empty', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderDrawer();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('gcp-connect-account-btn')).toBeEnabled();
|
||||
});
|
||||
|
||||
await user.click(screen.getByTestId('gcp-connect-account-btn'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Please enter an account name')).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.getByText('Please enter the deployment project ID'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Please select a region')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Please add at least one project ID'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(createAccountPayload).toBeNull();
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates the account, checks the agent in, and closes the drawer', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderDrawer();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('gcp-connect-account-btn')).toBeEnabled();
|
||||
});
|
||||
|
||||
await user.type(
|
||||
screen.getByTestId('gcp-account-name-input'),
|
||||
'billing@company.com',
|
||||
);
|
||||
await user.type(
|
||||
screen.getByTestId('gcp-deployment-project-id-input'),
|
||||
'my-deployment-project-123',
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId('gcp-deployment-region-select'));
|
||||
await user.click(await screen.findByText('Mumbai (asia-south1)'));
|
||||
|
||||
const projectIdsInput = document.querySelector(
|
||||
'#gcp-project-ids-select',
|
||||
) as HTMLInputElement;
|
||||
await user.type(projectIdsInput, 'project-a,project-b,');
|
||||
|
||||
await user.click(screen.getByTestId('gcp-connect-account-btn'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createAccountPayload).not.toBeNull();
|
||||
});
|
||||
|
||||
expect(createAccountPayload).toStrictEqual({
|
||||
config: {
|
||||
gcp: {
|
||||
deploymentRegion: 'asia-south1',
|
||||
deploymentProjectId: 'my-deployment-project-123',
|
||||
projectIds: ['project-a', 'project-b'],
|
||||
},
|
||||
},
|
||||
// Backend-provided credentials win over anything in the form.
|
||||
credentials: connectionCredentials,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checkInPayload).toStrictEqual({
|
||||
providerAccountId: 'billing@company.com',
|
||||
cloudIntegrationId: CLOUD_INTEGRATION_ID,
|
||||
data: {},
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the backend error inline when account creation fails', async () => {
|
||||
server.use(
|
||||
rest.post(GCP_ACCOUNTS_URL, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(400),
|
||||
ctx.json({
|
||||
status: 'error',
|
||||
error: { message: 'deployment project id is not accessible' },
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderDrawer();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('gcp-connect-account-btn')).toBeEnabled();
|
||||
});
|
||||
|
||||
await user.type(screen.getByTestId('gcp-account-name-input'), 'my-org');
|
||||
await user.type(
|
||||
screen.getByTestId('gcp-deployment-project-id-input'),
|
||||
'my-deployment-project-123',
|
||||
);
|
||||
await user.click(screen.getByTestId('gcp-deployment-region-select'));
|
||||
await user.click(await screen.findByText('Mumbai (asia-south1)'));
|
||||
|
||||
const projectIdsInput = document.querySelector(
|
||||
'#gcp-project-ids-select',
|
||||
) as HTMLInputElement;
|
||||
await user.type(projectIdsInput, 'project-a,');
|
||||
|
||||
await user.click(screen.getByTestId('gcp-connect-account-btn'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('gcp-connect-error')).toHaveTextContent(
|
||||
'deployment project id is not accessible',
|
||||
);
|
||||
});
|
||||
expect(checkInPayload).toBeNull();
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
CloudAccount,
|
||||
GCPCloudAccountConfig,
|
||||
} from 'container/Integrations/types';
|
||||
|
||||
export const GCP_CREDENTIALS_URL =
|
||||
'http://localhost/api/v1/cloud_integrations/gcp/credentials';
|
||||
export const GCP_ACCOUNTS_URL =
|
||||
'http://localhost/api/v1/cloud_integrations/gcp/accounts';
|
||||
export const GCP_CHECK_IN_URL =
|
||||
'http://localhost/api/v1/cloud_integrations/gcp/accounts/check_in';
|
||||
|
||||
export const CLOUD_INTEGRATION_ID = 'ci-gcp-1234';
|
||||
|
||||
export const GCP_ACCOUNT_ID = 'acc-gcp-1';
|
||||
export const GCP_ACCOUNT_URL = `${GCP_ACCOUNTS_URL}/${GCP_ACCOUNT_ID}`;
|
||||
|
||||
export const gcpAccountConfig: GCPCloudAccountConfig = {
|
||||
deployment_region: 'asia-south1',
|
||||
deployment_project_id: 'my-deployment-project-123',
|
||||
project_ids: ['project-a', 'project-b'],
|
||||
};
|
||||
|
||||
export const gcpAccount: CloudAccount = {
|
||||
id: GCP_ACCOUNT_ID,
|
||||
cloud_account_id: 'gcp-cloud-1',
|
||||
providerAccountId: 'billing@company.com',
|
||||
config: gcpAccountConfig,
|
||||
status: { integration: { last_heartbeat_ts_ms: 1_700_000_000_000 } },
|
||||
};
|
||||
|
||||
export const listAccountsResponse = {
|
||||
status: 'success',
|
||||
data: { accounts: [] },
|
||||
};
|
||||
|
||||
/**
|
||||
* Credentials the backend hands out on SigNoz Cloud. When present the drawer
|
||||
* renders them read-only and sends them back verbatim on submit.
|
||||
*/
|
||||
export const connectionCredentials: CloudintegrationtypesCredentialsDTO = {
|
||||
sigNozApiUrl: 'https://tenant.signoz.cloud',
|
||||
sigNozApiKey: 'signoz-api-key-abc',
|
||||
ingestionUrl: 'https://ingest.us.signoz.cloud',
|
||||
ingestionKey: 'ingestion-key-xyz',
|
||||
};
|
||||
|
||||
export const connectionCredentialsResponse = {
|
||||
status: 'success',
|
||||
data: connectionCredentials,
|
||||
};
|
||||
|
||||
export const createAccountResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
id: CLOUD_INTEGRATION_ID,
|
||||
connectionArtifact: {},
|
||||
},
|
||||
};
|
||||
|
||||
export const checkInResponse = {
|
||||
status: 'success',
|
||||
data: {},
|
||||
};
|
||||
@@ -60,6 +60,44 @@ function RemoveIntegrationAccount({
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
let modalDescription: JSX.Element;
|
||||
if (cloudProvider === INTEGRATION_TYPES.AWS) {
|
||||
modalDescription = (
|
||||
<>
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your AWS account within the next ~15 minutes
|
||||
(cloudformation stacks named signoz-integration-telemetry-collection in
|
||||
enabled regions). <br />
|
||||
<br />
|
||||
After that, you can delete the cloudformation stack that was created
|
||||
manually when connecting this account.
|
||||
</>
|
||||
);
|
||||
} else if (cloudProvider === INTEGRATION_TYPES.GCP) {
|
||||
modalDescription = (
|
||||
<>
|
||||
Removing this account will stop SigNoz from monitoring it. <br />
|
||||
<br />
|
||||
Since you manage the GCP resources yourself, remember to manually tear down
|
||||
the OTel collector and Pub/Sub resources you created for this integration if
|
||||
you no longer need them.
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
modalDescription = (
|
||||
<>
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your Azure subscription within the next ~15 minutes
|
||||
(deployment stack named signoz-integration-telemetry will be deleted
|
||||
automatically). <br />
|
||||
<br />
|
||||
After that, you have to manually delete 'signoz-integration'
|
||||
deployment stack that was created while connecting this account (Takes ~20
|
||||
minutes to delete).
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="remove-integration-account-container">
|
||||
<Button
|
||||
@@ -84,28 +122,7 @@ function RemoveIntegrationAccount({
|
||||
loading: isRemoveIntegrationLoading,
|
||||
}}
|
||||
>
|
||||
{cloudProvider === INTEGRATION_TYPES.AWS ? (
|
||||
<>
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your AWS account within the next ~15 minutes
|
||||
(cloudformation stacks named signoz-integration-telemetry-collection in
|
||||
enabled regions). <br />
|
||||
<br />
|
||||
After that, you can delete the cloudformation stack that was created
|
||||
manually when connecting this account.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your Azure subscription within the next ~15 minutes
|
||||
(deployment stack named signoz-integration-telemetry will be deleted
|
||||
automatically). <br />
|
||||
<br />
|
||||
After that, you have to manually delete 'signoz-integration'
|
||||
deployment stack that was created while connecting this account (Takes ~20
|
||||
minutes to delete).
|
||||
</>
|
||||
)}
|
||||
{modalDescription}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -47,3 +47,27 @@ export function mapAccountDtoToAzureCloudAccount(
|
||||
providerAccountId: account.providerAccountId,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapAccountDtoToGcpCloudAccount(
|
||||
account: CloudintegrationtypesAccountDTO,
|
||||
): IntegrationCloudAccount | null {
|
||||
if (!account.providerAccountId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
cloud_account_id: account.id,
|
||||
config: {
|
||||
deployment_region: account.config?.gcp?.deploymentRegion ?? '',
|
||||
deployment_project_id: account.config?.gcp?.deploymentProjectId ?? '',
|
||||
project_ids: account.config?.gcp?.projectIds ?? [],
|
||||
},
|
||||
status: {
|
||||
integration: {
|
||||
last_heartbeat_ts_ms: account.agentReport?.timestampMillis ?? 0,
|
||||
},
|
||||
},
|
||||
providerAccountId: account.providerAccountId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import { ArrowLeft, MoveUpRight, RotateCw } from '@signozhq/icons';
|
||||
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
|
||||
|
||||
import CloudIntegration from '../CloudIntegration/CloudIntegration';
|
||||
import { INTEGRATION_TYPES } from '../constants';
|
||||
import { IntegrationType } from '../types';
|
||||
import { INTEGRATION_TYPES } from '../constants';
|
||||
import { handleContactSupport } from '../utils';
|
||||
import IntegrationDetailContent from './IntegrationDetailContent';
|
||||
import IntegrationDetailHeader from './IntegrationDetailHeader';
|
||||
@@ -24,6 +24,12 @@ import { getConnectionStatesFromConnectionStatus } from './utils';
|
||||
|
||||
import './IntegrationDetailPage.styles.scss';
|
||||
|
||||
const cloudIntegrationTypeById: Record<string, IntegrationType> = {
|
||||
[INTEGRATION_TYPES.AWS]: IntegrationType.AWS_SERVICES,
|
||||
[INTEGRATION_TYPES.AZURE]: IntegrationType.AZURE_SERVICES,
|
||||
[INTEGRATION_TYPES.GCP]: IntegrationType.GCP_SERVICES,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function IntegrationDetailPage(): JSX.Element {
|
||||
const history = useHistory();
|
||||
@@ -55,19 +61,8 @@ function IntegrationDetailPage(): JSX.Element {
|
||||
),
|
||||
);
|
||||
|
||||
if (
|
||||
integrationId === INTEGRATION_TYPES.AWS ||
|
||||
integrationId === INTEGRATION_TYPES.AZURE
|
||||
) {
|
||||
return (
|
||||
<CloudIntegration
|
||||
type={
|
||||
integrationId === INTEGRATION_TYPES.AWS
|
||||
? IntegrationType.AWS_SERVICES
|
||||
: IntegrationType.AZURE_SERVICES
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (integrationId && cloudIntegrationTypeById[integrationId]) {
|
||||
return <CloudIntegration type={cloudIntegrationTypeById[integrationId]} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import awsDarkLogo from '@/assets/Logos/aws-dark.svg';
|
||||
import azureOpenaiLogo from '@/assets/Logos/azure-openai.svg';
|
||||
import gcpLogo from '@/assets/Logos/gcp.svg';
|
||||
|
||||
import { AzureRegion } from './types';
|
||||
import { AzureRegion, GCPRegion } from './types';
|
||||
|
||||
export const INTEGRATION_TELEMETRY_EVENTS = {
|
||||
INTEGRATIONS_LIST_VISITED: 'Integrations Page: Visited the list page',
|
||||
@@ -21,6 +22,7 @@ export const INTEGRATION_TELEMETRY_EVENTS = {
|
||||
export const INTEGRATION_TYPES = {
|
||||
AWS: 'aws',
|
||||
AZURE: 'azure',
|
||||
GCP: 'gcp',
|
||||
};
|
||||
|
||||
export const AWS_INTEGRATION = {
|
||||
@@ -53,7 +55,26 @@ export const AZURE_INTEGRATION = {
|
||||
is_new: true,
|
||||
};
|
||||
|
||||
export const ONE_CLICK_INTEGRATIONS = [AWS_INTEGRATION, AZURE_INTEGRATION];
|
||||
export const GCP_INTEGRATION = {
|
||||
id: INTEGRATION_TYPES.GCP,
|
||||
title: 'Google Cloud Platform',
|
||||
description: 'Setup for GCP monitoring with SigNoz',
|
||||
author: {
|
||||
name: 'SigNoz',
|
||||
email: 'integrations@signoz.io',
|
||||
homepage: 'https://signoz.io',
|
||||
},
|
||||
icon: gcpLogo,
|
||||
icon_alt: 'gcp-logo',
|
||||
is_installed: false,
|
||||
is_new: true,
|
||||
};
|
||||
|
||||
export const ONE_CLICK_INTEGRATIONS = [
|
||||
AWS_INTEGRATION,
|
||||
AZURE_INTEGRATION,
|
||||
GCP_INTEGRATION,
|
||||
];
|
||||
|
||||
export const AZURE_REGIONS: AzureRegion[] = [
|
||||
{
|
||||
@@ -165,3 +186,66 @@ export const AZURE_REGIONS: AzureRegion[] = [
|
||||
{ label: 'West US 2', value: 'westus2', geography: 'United States' },
|
||||
{ label: 'West US 3', value: 'westus3', geography: 'United States' },
|
||||
];
|
||||
|
||||
// Source of truth: pkg/types/cloudintegrationtypes/regions.go (GCP regions).
|
||||
export const GCP_REGIONS: GCPRegion[] = [
|
||||
{ label: 'Johannesburg', value: 'africa-south1', geography: 'Africa' },
|
||||
{ label: 'Changhua County', value: 'asia-east1', geography: 'APAC' },
|
||||
{ label: 'Hong Kong', value: 'asia-east2', geography: 'APAC' },
|
||||
{ label: 'Tokyo', value: 'asia-northeast1', geography: 'APAC' },
|
||||
{ label: 'Osaka', value: 'asia-northeast2', geography: 'APAC' },
|
||||
{ label: 'Seoul', value: 'asia-northeast3', geography: 'APAC' },
|
||||
{ label: 'Mumbai', value: 'asia-south1', geography: 'APAC' },
|
||||
{ label: 'Delhi', value: 'asia-south2', geography: 'APAC' },
|
||||
{ label: 'Singapore', value: 'asia-southeast1', geography: 'APAC' },
|
||||
{ label: 'Jakarta', value: 'asia-southeast2', geography: 'APAC' },
|
||||
{ label: 'Bangkok', value: 'asia-southeast3', geography: 'APAC' },
|
||||
{ label: 'Sydney', value: 'australia-southeast1', geography: 'APAC' },
|
||||
{ label: 'Melbourne', value: 'australia-southeast2', geography: 'APAC' },
|
||||
{ label: 'Warsaw', value: 'europe-central2', geography: 'Europe' },
|
||||
{ label: 'Hamina', value: 'europe-north1', geography: 'Europe' },
|
||||
{ label: 'Stockholm', value: 'europe-north2', geography: 'Europe' },
|
||||
{ label: 'Madrid', value: 'europe-southwest1', geography: 'Europe' },
|
||||
{ label: 'St. Ghislain', value: 'europe-west1', geography: 'Europe' },
|
||||
{ label: 'London', value: 'europe-west2', geography: 'Europe' },
|
||||
{ label: 'Frankfurt', value: 'europe-west3', geography: 'Europe' },
|
||||
{ label: 'Eemshaven', value: 'europe-west4', geography: 'Europe' },
|
||||
{ label: 'Zurich', value: 'europe-west6', geography: 'Europe' },
|
||||
{ label: 'Milan', value: 'europe-west8', geography: 'Europe' },
|
||||
{ label: 'Paris', value: 'europe-west9', geography: 'Europe' },
|
||||
{ label: 'Berlin', value: 'europe-west10', geography: 'Europe' },
|
||||
{ label: 'Turin', value: 'europe-west12', geography: 'Europe' },
|
||||
{ label: 'Doha', value: 'me-central1', geography: 'Middle East' },
|
||||
{ label: 'Dammam', value: 'me-central2', geography: 'Middle East' },
|
||||
{ label: 'Tel Aviv', value: 'me-west1', geography: 'Middle East' },
|
||||
{
|
||||
label: 'Montréal',
|
||||
value: 'northamerica-northeast1',
|
||||
geography: 'North America',
|
||||
},
|
||||
{
|
||||
label: 'Toronto',
|
||||
value: 'northamerica-northeast2',
|
||||
geography: 'North America',
|
||||
},
|
||||
{
|
||||
label: 'Querétaro',
|
||||
value: 'northamerica-south1',
|
||||
geography: 'North America',
|
||||
},
|
||||
{
|
||||
label: 'São Paulo',
|
||||
value: 'southamerica-east1',
|
||||
geography: 'South America',
|
||||
},
|
||||
{ label: 'Santiago', value: 'southamerica-west1', geography: 'South America' },
|
||||
{ label: 'Council Bluffs', value: 'us-central1', geography: 'North America' },
|
||||
{ label: 'Moncks Corner', value: 'us-east1', geography: 'North America' },
|
||||
{ label: 'Ashburn', value: 'us-east4', geography: 'North America' },
|
||||
{ label: 'Columbus', value: 'us-east5', geography: 'North America' },
|
||||
{ label: 'Dallas', value: 'us-south1', geography: 'North America' },
|
||||
{ label: 'The Dalles', value: 'us-west1', geography: 'North America' },
|
||||
{ label: 'Los Angeles', value: 'us-west2', geography: 'North America' },
|
||||
{ label: 'Salt Lake City', value: 'us-west3', geography: 'North America' },
|
||||
{ label: 'Las Vegas', value: 'us-west4', geography: 'North America' },
|
||||
];
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
export enum IntegrationType {
|
||||
AWS_SERVICES = 'aws',
|
||||
AZURE_SERVICES = 'azure',
|
||||
GCP_SERVICES = 'gcp',
|
||||
}
|
||||
|
||||
interface LogField {
|
||||
@@ -87,7 +88,10 @@ export interface ServiceData {
|
||||
export interface CloudAccount {
|
||||
id: string;
|
||||
cloud_account_id: string;
|
||||
config: AzureCloudAccountConfig | AWSCloudAccountConfig;
|
||||
config:
|
||||
| AzureCloudAccountConfig
|
||||
| AWSCloudAccountConfig
|
||||
| GCPCloudAccountConfig;
|
||||
status: AccountStatus | IServiceStatus;
|
||||
providerAccountId: string;
|
||||
}
|
||||
@@ -97,6 +101,12 @@ export interface AzureCloudAccountConfig {
|
||||
resource_groups: string[];
|
||||
}
|
||||
|
||||
export interface GCPCloudAccountConfig {
|
||||
deployment_region: string;
|
||||
deployment_project_id: string;
|
||||
project_ids: string[];
|
||||
}
|
||||
|
||||
export interface AccountStatus {
|
||||
integration: IntegrationStatus;
|
||||
}
|
||||
@@ -111,6 +121,12 @@ export interface AzureRegion {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface GCPRegion {
|
||||
label: string;
|
||||
geography: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface UpdateServiceConfigPayload {
|
||||
cloud_account_id: string;
|
||||
config: AzureServicesConfig;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
.explorer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-2) var(--spacing-0);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import styles from './Explorer.module.scss';
|
||||
|
||||
// Shell for the AI Observability Explorer tab. Owns the
|
||||
// /ai-observability/explorer route and is intentionally empty for now: the
|
||||
// query builder + results surface land in a follow-up.
|
||||
function Explorer(): JSX.Element {
|
||||
return (
|
||||
<div className={styles.explorer} data-testid="llm-observability-explorer">
|
||||
<div className={styles.placeholder}>Explorer coming soon.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Explorer;
|
||||
@@ -44,6 +44,7 @@ describe('LLMObservability (integration)', () => {
|
||||
expect(screen.getByTestId('llm-observability-overview')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('llm-overview-dashboard')).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Overview' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Explorer' })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('tab', { name: 'Model pricing' }),
|
||||
).toBeInTheDocument();
|
||||
@@ -78,6 +79,27 @@ describe('LLMObservability (integration)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('navigates to the explorer route when the Explorer tab is clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<LLMObservability />, undefined, {
|
||||
initialRoute: ROUTES.AI_OBSERVABILITY_OVERVIEW,
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('tab', { name: 'Explorer' }));
|
||||
|
||||
expect(safeNavigateMock).toHaveBeenCalledWith(
|
||||
ROUTES.AI_OBSERVABILITY_EXPLORER,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the explorer panel on the explorer route', () => {
|
||||
render(<LLMObservability />, undefined, {
|
||||
initialRoute: ROUTES.AI_OBSERVABILITY_EXPLORER,
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('llm-observability-explorer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the attribute mapping page on the attribute mapping route', () => {
|
||||
render(<LLMObservability />, undefined, {
|
||||
initialRoute: ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING,
|
||||
|
||||
@@ -5,10 +5,12 @@ import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
|
||||
import LLMObservabilityAttributeMapping from '../AttributeMapping/LLMObservabilityAttributeMapping';
|
||||
import Explorer from '../Explorer/Explorer';
|
||||
import Overview from '../Overview/Overview';
|
||||
import LLMObservabilityModelPricing from '../Settings/ModelPricing/LLMObservabilityModelPricing';
|
||||
|
||||
const OVERVIEW_KEY = ROUTES.AI_OBSERVABILITY_OVERVIEW;
|
||||
const EXPLORER_KEY = ROUTES.AI_OBSERVABILITY_EXPLORER;
|
||||
const CONFIGURATION_KEY = ROUTES.AI_OBSERVABILITY_CONFIGURATION;
|
||||
const ATTRIBUTE_MAPPING_KEY = ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING;
|
||||
|
||||
@@ -31,6 +33,8 @@ export function useLLMObservabilityTabs(): UseLLMObservabilityTabsResult {
|
||||
activeTab = CONFIGURATION_KEY;
|
||||
} else if (pathname.startsWith(ATTRIBUTE_MAPPING_KEY)) {
|
||||
activeTab = ATTRIBUTE_MAPPING_KEY;
|
||||
} else if (pathname.startsWith(EXPLORER_KEY)) {
|
||||
activeTab = EXPLORER_KEY;
|
||||
}
|
||||
|
||||
const onTabChange = useCallback(
|
||||
@@ -46,6 +50,11 @@ export function useLLMObservabilityTabs(): UseLLMObservabilityTabsResult {
|
||||
label: 'Overview',
|
||||
children: <Overview />,
|
||||
},
|
||||
{
|
||||
key: EXPLORER_KEY,
|
||||
label: 'Explorer',
|
||||
children: <Explorer />,
|
||||
},
|
||||
{
|
||||
key: CONFIGURATION_KEY,
|
||||
label: 'Model pricing',
|
||||
|
||||
@@ -14,7 +14,7 @@ import './InviteTeamMembers.styles.scss';
|
||||
|
||||
interface TeamMember {
|
||||
email: string;
|
||||
role: string;
|
||||
roles: string[];
|
||||
name: string;
|
||||
frontendBaseUrl: string;
|
||||
id: string;
|
||||
@@ -45,7 +45,7 @@ function InviteTeamMembers({
|
||||
const toTeamMembers = (rows: InviteMemberRow[]): TeamMember[] =>
|
||||
rows.map((row) => ({
|
||||
email: row.email,
|
||||
role: roleIdToName[row.roleId] ?? row.roleId,
|
||||
roles: row.roleIds.map((roleId) => roleIdToName[roleId] ?? roleId),
|
||||
name: '',
|
||||
frontendBaseUrl: getBaseUrl(),
|
||||
id: row.id,
|
||||
|
||||
@@ -166,8 +166,8 @@ describe('InviteTeamMembers', () => {
|
||||
{ email: 'user2@test.com', success: true },
|
||||
];
|
||||
const mockRows: InviteMemberRow[] = [
|
||||
{ id: 'row-1', email: 'user1@test.com', roleId: 'role-viewer-id' },
|
||||
{ id: 'row-2', email: 'user2@test.com', roleId: 'role-editor-id' },
|
||||
{ id: 'row-1', email: 'user1@test.com', roleIds: ['role-viewer-id'] },
|
||||
{ id: 'row-2', email: 'user2@test.com', roleIds: ['role-editor-id'] },
|
||||
];
|
||||
mockInviteMembersProps?.onSuccess?.(mockResults, mockRows);
|
||||
|
||||
@@ -177,14 +177,14 @@ describe('InviteTeamMembers', () => {
|
||||
teamMembers: [
|
||||
{
|
||||
email: 'user1@test.com',
|
||||
role: 'VIEWER',
|
||||
roles: ['VIEWER'],
|
||||
name: '',
|
||||
frontendBaseUrl: 'http://localhost:3301',
|
||||
id: 'row-1',
|
||||
},
|
||||
{
|
||||
email: 'user2@test.com',
|
||||
role: 'EDITOR',
|
||||
roles: ['EDITOR'],
|
||||
name: '',
|
||||
frontendBaseUrl: 'http://localhost:3301',
|
||||
id: 'row-2',
|
||||
@@ -211,8 +211,8 @@ describe('InviteTeamMembers', () => {
|
||||
{ email: 'user2@test.com', success: false, error: 'Already exists' },
|
||||
];
|
||||
const mockRows: InviteMemberRow[] = [
|
||||
{ id: 'row-1', email: 'user1@test.com', roleId: 'role-viewer-id' },
|
||||
{ id: 'row-2', email: 'user2@test.com', roleId: 'role-admin-id' },
|
||||
{ id: 'row-1', email: 'user1@test.com', roleIds: ['role-viewer-id'] },
|
||||
{ id: 'row-2', email: 'user2@test.com', roleIds: ['role-admin-id'] },
|
||||
];
|
||||
mockInviteMembersProps?.onPartialSuccess?.(mockResults, mockRows);
|
||||
|
||||
@@ -222,14 +222,14 @@ describe('InviteTeamMembers', () => {
|
||||
teamMembers: [
|
||||
{
|
||||
email: 'user1@test.com',
|
||||
role: 'VIEWER',
|
||||
roles: ['VIEWER'],
|
||||
name: '',
|
||||
frontendBaseUrl: 'http://localhost:3301',
|
||||
id: 'row-1',
|
||||
},
|
||||
{
|
||||
email: 'user2@test.com',
|
||||
role: 'ADMIN',
|
||||
roles: ['ADMIN'],
|
||||
name: '',
|
||||
frontendBaseUrl: 'http://localhost:3301',
|
||||
id: 'row-2',
|
||||
@@ -252,8 +252,8 @@ describe('InviteTeamMembers', () => {
|
||||
{ email: 'user2@test.com', success: false, error: 'Error 2' },
|
||||
];
|
||||
const mockRows: InviteMemberRow[] = [
|
||||
{ id: 'row-1', email: 'user1@test.com', roleId: 'role-editor-id' },
|
||||
{ id: 'row-2', email: 'user2@test.com', roleId: 'role-viewer-id' },
|
||||
{ id: 'row-1', email: 'user1@test.com', roleIds: ['role-editor-id'] },
|
||||
{ id: 'row-2', email: 'user2@test.com', roleIds: ['role-viewer-id'] },
|
||||
];
|
||||
mockInviteMembersProps?.onAllFailed?.(mockResults, mockRows);
|
||||
|
||||
@@ -263,14 +263,14 @@ describe('InviteTeamMembers', () => {
|
||||
teamMembers: [
|
||||
{
|
||||
email: 'user1@test.com',
|
||||
role: 'EDITOR',
|
||||
roles: ['EDITOR'],
|
||||
name: '',
|
||||
frontendBaseUrl: 'http://localhost:3301',
|
||||
id: 'row-1',
|
||||
},
|
||||
{
|
||||
email: 'user2@test.com',
|
||||
role: 'VIEWER',
|
||||
roles: ['VIEWER'],
|
||||
name: '',
|
||||
frontendBaseUrl: 'http://localhost:3301',
|
||||
id: 'row-2',
|
||||
|
||||
@@ -33,7 +33,7 @@ jest.mock('hooks/useNotifications', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const RESET_PASSWORD_ENDPOINT = '*/resetPassword';
|
||||
const RESET_PASSWORD_ENDPOINT = '*/api/v2/factor_password/reset';
|
||||
|
||||
const mockHistoryPush = history.push as jest.MockedFunction<
|
||||
typeof history.push
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-use';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
import { Form, Input as AntdInput } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import resetPasswordApi from 'api/v1/factor_password/resetPassword';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { useResetPassword } from 'api/generated/services/users';
|
||||
import AuthError from 'components/AuthError/AuthError';
|
||||
import AuthPageContainer from 'components/AuthPageContainer';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -14,7 +15,6 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { ArrowRight, CircleAlert, KeyRound } from '@signozhq/icons';
|
||||
import { Label } from 'pages/SignUp/styles';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { FormContainer } from './styles';
|
||||
|
||||
@@ -26,40 +26,41 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
const [confirmPasswordError, setConfirmPasswordError] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<APIError | null>();
|
||||
|
||||
const [isValidPassword, setIsValidPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation(['common']);
|
||||
const { search } = useLocation();
|
||||
const params = new URLSearchParams(search);
|
||||
const token = params.get('token');
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const {
|
||||
mutate: resetPassword,
|
||||
isLoading,
|
||||
error: mutationError,
|
||||
} = useResetPassword();
|
||||
|
||||
const errorMessage = useMemo(
|
||||
() => convertToApiError(mutationError),
|
||||
[mutationError],
|
||||
);
|
||||
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const handleFormSubmit: () => Promise<void> = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setErrorMessage(null);
|
||||
const { password } = form.getFieldsValue();
|
||||
const handleFormSubmit = (): void => {
|
||||
const { password } = form.getFieldsValue();
|
||||
|
||||
await resetPasswordApi({
|
||||
password,
|
||||
token: token || '',
|
||||
});
|
||||
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
history.push(ROUTES.LOGIN);
|
||||
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setErrorMessage(error as APIError);
|
||||
}
|
||||
resetPassword(
|
||||
{ data: { password, token: token || '' } },
|
||||
{
|
||||
onSuccess: (): void => {
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
history.push(ROUTES.LOGIN);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const validatePassword = (): boolean => {
|
||||
@@ -222,7 +223,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
color="primary"
|
||||
type="submit"
|
||||
data-attr="reset-password"
|
||||
disabled={!isValidPassword || loading}
|
||||
disabled={!isValidPassword || isLoading}
|
||||
className="reset-password-submit-button"
|
||||
suffix={<ArrowRight size={16} />}
|
||||
>
|
||||
|
||||
@@ -206,6 +206,7 @@ export const routesToSkip = [
|
||||
ROUTES.AI_OBSERVABILITY_OVERVIEW,
|
||||
ROUTES.AI_OBSERVABILITY_CONFIGURATION,
|
||||
ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING,
|
||||
ROUTES.AI_OBSERVABILITY_EXPLORER,
|
||||
];
|
||||
|
||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
export function isOneClickIntegration(integrationId: string): boolean {
|
||||
return (
|
||||
integrationId === INTEGRATION_TYPES.AWS ||
|
||||
integrationId === INTEGRATION_TYPES.AZURE
|
||||
integrationId === INTEGRATION_TYPES.AZURE ||
|
||||
integrationId === INTEGRATION_TYPES.GCP
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,12 @@ export function useAccountSettingsModal({
|
||||
}: UseAccountSettingsModalProps): UseAccountSettingsModal {
|
||||
const [form] = Form.useForm();
|
||||
const { mutate: updateAccount, isLoading } = useUpdateAccount();
|
||||
// `account.config` is the shared per-provider union (Azure | AWS | GCP).
|
||||
// Narrow to Azure by `resource_groups` (Azure-only) rather than
|
||||
// `deployment_region`, which GCP also has — so it no longer identifies
|
||||
// Azure uniquely.
|
||||
const accountConfig = useMemo(
|
||||
() => ('deployment_region' in account.config ? account.config : null),
|
||||
() => ('resource_groups' in account.config ? account.config : null),
|
||||
[account.config],
|
||||
);
|
||||
const [resourceGroups, setResourceGroups] = useState<string[]>(
|
||||
|
||||
148
frontend/src/hooks/integration/gcp/useAccountSettingsDrawer.ts
Normal file
148
frontend/src/hooks/integration/gcp/useAccountSettingsDrawer.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Form } from 'antd';
|
||||
import { FormInstance } from 'antd/lib';
|
||||
import { useUpdateAccount } from 'api/generated/services/cloudintegration';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { CloudAccount } from 'container/Integrations/types';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
|
||||
interface UseAccountSettingsDrawerProps {
|
||||
onClose: () => void;
|
||||
account: CloudAccount;
|
||||
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
|
||||
}
|
||||
|
||||
interface UseAccountSettingsDrawer {
|
||||
form: FormInstance;
|
||||
isLoading: boolean;
|
||||
projectIds: string[];
|
||||
isSaveDisabled: boolean;
|
||||
setProjectIds: Dispatch<SetStateAction<string[]>>;
|
||||
handleSubmit: () => Promise<void>;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
export function useAccountSettingsDrawer({
|
||||
onClose,
|
||||
account,
|
||||
setActiveAccount,
|
||||
}: UseAccountSettingsDrawerProps): UseAccountSettingsDrawer {
|
||||
const [form] = Form.useForm();
|
||||
const { mutate: updateAccount, isLoading } = useUpdateAccount();
|
||||
const accountConfig = useMemo(
|
||||
() => ('project_ids' in account.config ? account.config : null),
|
||||
[account.config],
|
||||
);
|
||||
const [projectIds, setProjectIds] = useState<string[]>(
|
||||
accountConfig?.project_ids || [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
projectIds: accountConfig.project_ids,
|
||||
});
|
||||
setProjectIds(accountConfig.project_ids);
|
||||
}, [accountConfig, form]);
|
||||
|
||||
const handleSubmit = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
if (!accountConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateAccount(
|
||||
{
|
||||
pathParams: {
|
||||
cloudProvider: INTEGRATION_TYPES.GCP,
|
||||
id: account?.id || '',
|
||||
},
|
||||
data: {
|
||||
config: {
|
||||
gcp: {
|
||||
// Deployment region & project ID are immutable in the UI, but the
|
||||
// Updatable GCP DTO requires all three fields to be sent.
|
||||
deploymentRegion: accountConfig.deployment_region,
|
||||
deploymentProjectId: accountConfig.deployment_project_id,
|
||||
projectIds: values.projectIds || [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
const nextConfig = {
|
||||
deployment_region: accountConfig.deployment_region,
|
||||
deployment_project_id: accountConfig.deployment_project_id,
|
||||
project_ids: values.projectIds || [],
|
||||
};
|
||||
|
||||
setActiveAccount({
|
||||
...account,
|
||||
config: nextConfig,
|
||||
});
|
||||
onClose();
|
||||
|
||||
toast.success('Account settings updated successfully', {
|
||||
position: 'bottom-right',
|
||||
});
|
||||
|
||||
void logEvent('GCP Integration: Account settings updated', {
|
||||
cloudAccountId: account.cloud_account_id,
|
||||
deploymentRegion: nextConfig.deployment_region,
|
||||
projectIds: nextConfig.project_ids,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to update account settings', {
|
||||
description: error?.message,
|
||||
position: 'bottom-right',
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Form submission failed:', error);
|
||||
}
|
||||
}, [form, updateAccount, account, accountConfig, setActiveAccount, onClose]);
|
||||
|
||||
const isSaveDisabled = useMemo(() => {
|
||||
if (!accountConfig) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isEqual(
|
||||
[...(projectIds || [])].sort(),
|
||||
[...accountConfig.project_ids].sort(),
|
||||
);
|
||||
}, [accountConfig, projectIds]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
return {
|
||||
form,
|
||||
isLoading,
|
||||
projectIds,
|
||||
isSaveDisabled,
|
||||
setProjectIds,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
};
|
||||
}
|
||||
165
frontend/src/hooks/integration/gcp/useCloudAccountSetupDrawer.ts
Normal file
165
frontend/src/hooks/integration/gcp/useCloudAccountSetupDrawer.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import {
|
||||
CreateAccountMutationResult,
|
||||
GetConnectionCredentialsQueryResult,
|
||||
invalidateListAccounts,
|
||||
useAgentCheckIn,
|
||||
useCreateAccount,
|
||||
useGetConnectionCredentials,
|
||||
} from 'api/generated/services/cloudintegration';
|
||||
import {
|
||||
CloudintegrationtypesCredentialsDTO,
|
||||
CloudintegrationtypesPostableAccountDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { ErrorType } from 'api/generatedAPIInstance';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { GcpSetupFormValues } from 'container/Integrations/CloudIntegration/GoogleCloudPlatform/AddNewAccount/types';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
|
||||
interface UseCloudAccountSetupDrawerProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface UseCloudAccountSetupDrawer {
|
||||
isLoading: boolean;
|
||||
connectAccount: (values: GcpSetupFormValues) => Promise<void>;
|
||||
handleClose: () => void;
|
||||
connectionParams?: CloudintegrationtypesCredentialsDTO;
|
||||
isConnectionParamsLoading: boolean;
|
||||
submitError: string | null;
|
||||
clearSubmitError: () => void;
|
||||
}
|
||||
|
||||
export function useCloudAccountSetupDrawer({
|
||||
onClose,
|
||||
}: UseCloudAccountSetupDrawerProps): UseCloudAccountSetupDrawer {
|
||||
const queryClient = useQueryClient();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
|
||||
const clearSubmitError = useCallback((): void => {
|
||||
setSubmitError(null);
|
||||
}, []);
|
||||
|
||||
const { mutateAsync: createAccount } = useCreateAccount();
|
||||
const { mutateAsync: checkIn } = useAgentCheckIn();
|
||||
const handleError = useAxiosError();
|
||||
|
||||
const { data: connectionParams, isLoading: isConnectionParamsLoading } =
|
||||
useGetConnectionCredentials<GetConnectionCredentialsQueryResult>(
|
||||
{
|
||||
cloudProvider: INTEGRATION_TYPES.GCP,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
onError: handleError,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
const handleConnectionSuccess = useCallback(
|
||||
(payload: {
|
||||
cloudIntegrationId: string;
|
||||
providerAccountId: string;
|
||||
}): void => {
|
||||
void logEvent('GCP Integration: Account connected', {
|
||||
cloudIntegrationId: payload.cloudIntegrationId,
|
||||
providerAccountId: payload.providerAccountId,
|
||||
});
|
||||
toast.success('GCP account connected successfully', {
|
||||
position: 'bottom-right',
|
||||
});
|
||||
void invalidateListAccounts(queryClient, {
|
||||
cloudProvider: INTEGRATION_TYPES.GCP,
|
||||
});
|
||||
handleClose();
|
||||
},
|
||||
[handleClose, queryClient],
|
||||
);
|
||||
|
||||
const connectAccount = useCallback(
|
||||
async (values: GcpSetupFormValues): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setSubmitError(null);
|
||||
|
||||
const payload: CloudintegrationtypesPostableAccountDTO = {
|
||||
config: {
|
||||
gcp: {
|
||||
deploymentRegion: values.deploymentRegion,
|
||||
deploymentProjectId: values.deploymentProjectId,
|
||||
projectIds: values.projectIds || [],
|
||||
},
|
||||
},
|
||||
credentials: {
|
||||
// Cloud users can't edit these — the backend-provided credentials are
|
||||
// authoritative. Enterprise users have no backend defaults and enter
|
||||
// their own (validated non-empty), so their form values are used.
|
||||
ingestionUrl: connectionParams?.data?.ingestionUrl || values.ingestionUrl,
|
||||
ingestionKey: connectionParams?.data?.ingestionKey || values.ingestionKey,
|
||||
sigNozApiUrl: connectionParams?.data?.sigNozApiUrl || values.sigNozApiUrl,
|
||||
sigNozApiKey: connectionParams?.data?.sigNozApiKey || values.sigNozApiKey,
|
||||
},
|
||||
};
|
||||
|
||||
// Step 1: create the integration account.
|
||||
const createResponse: CreateAccountMutationResult = await createAccount({
|
||||
pathParams: { cloudProvider: INTEGRATION_TYPES.GCP },
|
||||
data: payload,
|
||||
});
|
||||
|
||||
const cloudIntegrationId = createResponse.data.id;
|
||||
const providerAccountId = values.accountName;
|
||||
|
||||
void logEvent('GCP Integration: Account created', {
|
||||
id: cloudIntegrationId,
|
||||
});
|
||||
|
||||
// Step 2: mimic the agent by checking in from the frontend (manual flow).
|
||||
await checkIn({
|
||||
pathParams: { cloudProvider: INTEGRATION_TYPES.GCP },
|
||||
data: {
|
||||
providerAccountId,
|
||||
cloudIntegrationId,
|
||||
data: {},
|
||||
},
|
||||
});
|
||||
|
||||
handleConnectionSuccess({ cloudIntegrationId, providerAccountId });
|
||||
} catch (error) {
|
||||
// Surface the backend's message inline in the drawer instead of a
|
||||
// generic failure string.
|
||||
const message = toAPIError(
|
||||
error as ErrorType<RenderErrorResponseDTO>,
|
||||
'Failed to connect GCP account',
|
||||
).getErrorMessage();
|
||||
setSubmitError(message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[connectionParams, createAccount, checkIn, handleConnectionSuccess],
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
connectAccount,
|
||||
handleClose,
|
||||
connectionParams: connectionParams?.data as
|
||||
| CloudintegrationtypesCredentialsDTO
|
||||
| undefined,
|
||||
isConnectionParamsLoading,
|
||||
submitError,
|
||||
clearSubmitError,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MouseEventHandler } from 'react';
|
||||
import { MouseEvent } from 'react';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
@@ -11,7 +11,7 @@ export type UseCopyLogLink = {
|
||||
isHighlighted: boolean;
|
||||
isLogsExplorerPage: boolean;
|
||||
activeLogId: string | null;
|
||||
onLogCopy: MouseEventHandler<HTMLElement>;
|
||||
onLogCopy: (event?: MouseEvent<HTMLElement>) => void;
|
||||
onClearActiveLog: () => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { MouseEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -46,14 +40,14 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const onLogCopy: MouseEventHandler<HTMLElement> = useCallback(
|
||||
(event) => {
|
||||
const onLogCopy = useCallback(
|
||||
(event?: MouseEvent<HTMLElement>): void => {
|
||||
if (!logId) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
urlQuery.delete(QueryParams.activeLogId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
@@ -66,7 +60,7 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
|
||||
setCopy(link);
|
||||
|
||||
toast.success('Copied to clipboard', { position: 'top-right' });
|
||||
toast.success('Copied to clipboard', { position: 'bottom-right' });
|
||||
},
|
||||
[logId, urlQuery, minTime, maxTime, pathname, setCopy],
|
||||
);
|
||||
|
||||
@@ -139,27 +139,6 @@ export const handlers = [
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
rest.get('http://localhost/api/v1/loginPrecheck', (req, res, ctx) => {
|
||||
const email = req.url.searchParams.get('email');
|
||||
if (email === 'failEmail@signoz.io') {
|
||||
return res(ctx.status(500));
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
sso: true,
|
||||
ssoUrl: '',
|
||||
canSelfRegister: false,
|
||||
isUser: true,
|
||||
ssoError: '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}),
|
||||
|
||||
rest.get('http://localhost/api/v2/licenses', (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(licensesSuccessResponse)),
|
||||
),
|
||||
@@ -194,14 +173,6 @@ export const handlers = [
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.put('http://localhost/api/v1/user/:id', (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: 'user updated successfully',
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'http://localhost/api/v3/autocomplete/aggregate_attributes',
|
||||
(req, res, ctx) =>
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface CopyButtonProps {
|
||||
/** Extra class merged onto the button. */
|
||||
className?: string;
|
||||
testId?: string;
|
||||
/** Called after the copy is triggered (e.g. to show a toast). */
|
||||
onCopy?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,6 +31,7 @@ function CopyButton({
|
||||
ariaLabel = 'Copy',
|
||||
className,
|
||||
testId,
|
||||
onCopy,
|
||||
}: CopyButtonProps): JSX.Element {
|
||||
const { copyToClipboard, isCopied } = useCopyButton();
|
||||
|
||||
@@ -36,8 +39,9 @@ function CopyButton({
|
||||
(e: MouseEvent<HTMLButtonElement>): void => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(value);
|
||||
onCopy?.();
|
||||
},
|
||||
[copyToClipboard, value],
|
||||
[copyToClipboard, value, onCopy],
|
||||
);
|
||||
|
||||
const stackStyle: CSSProperties = { width: size, height: size };
|
||||
@@ -65,6 +69,7 @@ CopyButton.defaultProps = {
|
||||
ariaLabel: 'Copy',
|
||||
className: undefined,
|
||||
testId: undefined,
|
||||
onCopy: undefined,
|
||||
};
|
||||
|
||||
export default CopyButton;
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { User } from 'types/reducer/app';
|
||||
|
||||
export interface UserProps {
|
||||
name: User['displayName'];
|
||||
email: User['email'];
|
||||
role: string;
|
||||
frontendBaseUrl: string;
|
||||
}
|
||||
|
||||
export interface UsersProps {
|
||||
invites: UserProps[];
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface Props {
|
||||
token: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
data: string;
|
||||
status: string;
|
||||
}
|
||||
@@ -151,6 +151,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
AI_OBSERVABILITY_ATTRIBUTE_MAPPING: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
AI_OBSERVABILITY_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
AI_OBSERVABILITY_OVERVIEW: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
AI_OBSERVABILITY_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
AI_OBSERVABILITY_CONFIGURATION: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
};
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@@ -4,7 +4,7 @@ go 1.25.7
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2
|
||||
github.com/AfterShip/clickhouse-sql-parser v0.5.4
|
||||
github.com/AfterShip/clickhouse-sql-parser v0.5.5
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.44.0
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/SigNoz/clickhouse-go-mock v0.14.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -66,8 +66,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/AfterShip/clickhouse-sql-parser v0.5.4 h1:yiCQaMq8EO+dpKdnpP9YYd/ne6MSuOXgsMsNL33NiTI=
|
||||
github.com/AfterShip/clickhouse-sql-parser v0.5.4/go.mod h1:Qi3qvPTfZb/aFwI5V4WFOahgjsLJa4MzVijIAfwOhDw=
|
||||
github.com/AfterShip/clickhouse-sql-parser v0.5.5 h1:LCA23yAA4GgF73PoYXb67yzCdC4sXsj4geQz1Oij3U8=
|
||||
github.com/AfterShip/clickhouse-sql-parser v0.5.5/go.mod h1:Qi3qvPTfZb/aFwI5V4WFOahgjsLJa4MzVijIAfwOhDw=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/promote"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/savedview"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/session"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
@@ -75,6 +76,7 @@ type provider struct {
|
||||
rulerHandler ruler.Handler
|
||||
llmPricingRuleHandler llmpricingrule.Handler
|
||||
statsHandler statsreporter.Handler
|
||||
savedViewHandler savedview.Handler
|
||||
}
|
||||
|
||||
func NewFactory(
|
||||
@@ -110,6 +112,7 @@ func NewFactory(
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
rulerHandler ruler.Handler,
|
||||
statsHandler statsreporter.Handler,
|
||||
savedViewHandler savedview.Handler,
|
||||
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
|
||||
return newProvider(
|
||||
@@ -148,6 +151,7 @@ func NewFactory(
|
||||
traceDetailHandler,
|
||||
rulerHandler,
|
||||
statsHandler,
|
||||
savedViewHandler,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -188,6 +192,7 @@ func newProvider(
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
rulerHandler ruler.Handler,
|
||||
statsHandler statsreporter.Handler,
|
||||
savedViewHandler savedview.Handler,
|
||||
) (apiserver.APIServer, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
|
||||
router := mux.NewRouter().UseEncodedPath()
|
||||
@@ -227,6 +232,7 @@ func newProvider(
|
||||
rulerHandler: rulerHandler,
|
||||
llmPricingRuleHandler: llmPricingRuleHandler,
|
||||
statsHandler: statsHandler,
|
||||
savedViewHandler: savedViewHandler,
|
||||
}
|
||||
|
||||
provider.authzMiddleware = middleware.NewAuthZ(settings.Logger(), orgGetter, authzService)
|
||||
@@ -359,6 +365,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addSavedViewRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
151
pkg/apiserver/signozapiserver/savedview.go
Normal file
151
pkg/apiserver/signozapiserver/savedview.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addSavedViewRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/saved_views", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.savedViewHandler.ListV2, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListSavedViews",
|
||||
Tags: []string{"saved_view"},
|
||||
Summary: "List saved views",
|
||||
Description: "Returns saved views, optionally filtered by source and name.",
|
||||
Request: nil,
|
||||
RequestQuery: new(savedviewtypes.ListSavedViewsParams),
|
||||
RequestContentType: "",
|
||||
Response: new([]*savedviewtypes.SavedView),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceSavedView.Scope(coretypes.VerbList)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceSavedView,
|
||||
Verb: coretypes.VerbList,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}),
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/saved_views", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.savedViewHandler.CreateV2, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateSavedView",
|
||||
Tags: []string{"saved_view"},
|
||||
Summary: "Create saved view",
|
||||
Description: "Persists a saved view for the explore page. Returns the id of the created view.",
|
||||
Request: new(savedviewtypes.PostableSavedView),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceSavedView.Scope(coretypes.VerbCreate)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceSavedView,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
ID: coretypes.ResponseJSONPath("data.id"),
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/saved_views/{id}", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.savedViewHandler.GetV2, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetSavedView",
|
||||
Tags: []string{"saved_view"},
|
||||
Summary: "Get saved view",
|
||||
Description: "Returns a saved view by id.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(savedviewtypes.SavedView),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceSavedView.Scope(coretypes.VerbRead)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceSavedView,
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
ID: coretypes.PathParam("id"),
|
||||
Selector: coretypes.IDSelector,
|
||||
}),
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/saved_views/{id}", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.savedViewHandler.UpdateV2, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateSavedView",
|
||||
Tags: []string{"saved_view"},
|
||||
Summary: "Update saved view",
|
||||
Description: "Replaces a saved view's name and query.",
|
||||
Request: new(savedviewtypes.UpdatableSavedView),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceSavedView.Scope(coretypes.VerbUpdate)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceSavedView,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
ID: coretypes.PathParam("id"),
|
||||
Selector: coretypes.IDSelector,
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/saved_views/{id}", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.savedViewHandler.Delete, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteSavedView",
|
||||
Tags: []string{"saved_view"},
|
||||
Summary: "Delete saved view",
|
||||
Description: "Deletes a saved view by id.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceSavedView.Scope(coretypes.VerbDelete)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceSavedView,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
ID: coretypes.PathParam("id"),
|
||||
Selector: coretypes.IDSelector,
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -27,22 +27,6 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/invite/bulk", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.CreateBulkInvite), handler.OpenAPIDef{
|
||||
ID: "CreateBulkInvite",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Create bulk invite",
|
||||
Description: "This endpoint creates a bulk invite for a user",
|
||||
Request: new(types.PostableBulkInviteRequest),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: true,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/user", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.ListUsersDeprecated), handler.OpenAPIDef{
|
||||
ID: "ListUsersDeprecated",
|
||||
Tags: []string{"users"},
|
||||
@@ -145,23 +129,6 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authzMiddleware.SelfAccess(provider.userHandler.GetUserDeprecated), handler.OpenAPIDef{
|
||||
ID: "GetUserDeprecated",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Get user",
|
||||
Description: "This endpoint returns the user by id",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(types.DeprecatedUser),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: true,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.GetUser), handler.OpenAPIDef{
|
||||
ID: "GetUser",
|
||||
Tags: []string{"users"},
|
||||
@@ -179,23 +146,6 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authzMiddleware.SelfAccess(provider.userHandler.UpdateUserDeprecated), handler.OpenAPIDef{
|
||||
ID: "UpdateUserDeprecated",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Update user",
|
||||
Description: "This endpoint updates the user by id",
|
||||
Request: new(types.DeprecatedUser),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(types.DeprecatedUser),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: true,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.UpdateUser), handler.OpenAPIDef{
|
||||
ID: "UpdateUser",
|
||||
Tags: []string{"users"},
|
||||
@@ -213,23 +163,6 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.DeleteUser), handler.OpenAPIDef{
|
||||
ID: "DeleteUserDeprecated",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Delete user",
|
||||
Description: "This endpoint deletes the user by id",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: true,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.DeleteUser), handler.OpenAPIDef{
|
||||
ID: "DeleteUser",
|
||||
Tags: []string{"users"},
|
||||
@@ -316,7 +249,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/resetPassword", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.ResetPassword), handler.OpenAPIDef{
|
||||
ID: "ResetPassword",
|
||||
ID: "ResetPasswordDeprecated",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Reset password",
|
||||
Description: "This endpoint resets the password by token",
|
||||
@@ -326,7 +259,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
Deprecated: true,
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{},
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
@@ -366,6 +299,23 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/factor_password/reset", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.ResetPassword), handler.OpenAPIDef{
|
||||
ID: "ResetPassword",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Reset password",
|
||||
Description: "This endpoint resets the password using a single use reset password token",
|
||||
Request: new(types.PostableResetPassword),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{},
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/users/{id}/roles", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.GetRolesByUserID), handler.OpenAPIDef{
|
||||
ID: "GetRolesByUserID",
|
||||
Tags: []string{"users"},
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -151,40 +150,6 @@ func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc {
|
||||
})
|
||||
}
|
||||
|
||||
func (middleware *AuthZ) SelfAccess(next http.HandlerFunc) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(req.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
selectors := []coretypes.Selector{
|
||||
coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName),
|
||||
}
|
||||
|
||||
err = middleware.authzService.CheckWithTupleCreation(
|
||||
req.Context(),
|
||||
claims,
|
||||
valuer.MustNewUUID(claims.OrgID),
|
||||
authtypes.Relation{Verb: coretypes.VerbAssignee},
|
||||
coretypes.NewResourceRole(),
|
||||
selectors,
|
||||
selectors,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
id := mux.Vars(req)["id"]
|
||||
if err := claims.IsSelfAccess(id); err != nil {
|
||||
middleware.logger.WarnContext(req.Context(), authzDeniedMessage, slog.Any("claims", claims))
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
next(rw, req)
|
||||
})
|
||||
}
|
||||
|
||||
func (middleware *AuthZ) OpenAccess(next http.HandlerFunc) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
next(rw, req)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": true
|
||||
"logs": false
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": true
|
||||
"logs": false
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": true
|
||||
"logs": false
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user