mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-14 04:30:33 +01:00
Compare commits
230 Commits
v0.121.0
...
feat/dropd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48f1a4cbf3 | ||
|
|
bb499973bf | ||
|
|
72036b42e3 | ||
|
|
13c087d34d | ||
|
|
feb9031bcd | ||
|
|
bc4a6b7ded | ||
|
|
9216bb5f34 | ||
|
|
18d2806f95 | ||
|
|
8d666471e1 | ||
|
|
9d022772b7 | ||
|
|
648a48cbaa | ||
|
|
9301b2fb1c | ||
|
|
6d0e60822c | ||
|
|
acdaef6c2e | ||
|
|
a7690bdaa2 | ||
|
|
a7fde606ca | ||
|
|
0aaf556137 | ||
|
|
582ba1c677 | ||
|
|
3d8cddf84e | ||
|
|
ac46cd8e80 | ||
|
|
18d5e92ae2 | ||
|
|
5eaca31759 | ||
|
|
8b0ccc8ddc | ||
|
|
1118136b69 | ||
|
|
ae3f5114c4 | ||
|
|
8409a9798d | ||
|
|
de6e4890ae | ||
|
|
20dd264ac1 | ||
|
|
8a7793794d | ||
|
|
680bcd08c3 | ||
|
|
eb22c57a67 | ||
|
|
896379b680 | ||
|
|
f041b16e4b | ||
|
|
9ca3a7fd3e | ||
|
|
43e122367c | ||
|
|
5cf0e0fbb9 | ||
|
|
c6683e075e | ||
|
|
3bc936282e | ||
|
|
c3f44b31fe | ||
|
|
0c9f237369 | ||
|
|
8b13f004ed | ||
|
|
8c1d13bb38 | ||
|
|
ad8f3328e0 | ||
|
|
cc3da72aa5 | ||
|
|
755390c4b5 | ||
|
|
adbd89aae9 | ||
|
|
603077c575 | ||
|
|
7e5c4476f7 | ||
|
|
da648ed3f3 | ||
|
|
9fa56aacd1 | ||
|
|
5acd79419c | ||
|
|
9b7b0f8862 | ||
|
|
c29e8a0136 | ||
|
|
ebac945ac2 | ||
|
|
e787497695 | ||
|
|
eba6bd5f5b | ||
|
|
1aeab2718d | ||
|
|
d879af4fb3 | ||
|
|
ac10be2eb2 | ||
|
|
113d1544ba | ||
|
|
df02da664c | ||
|
|
d0a491ed8e | ||
|
|
77c39a9f05 | ||
|
|
309a76e5fd | ||
|
|
43e80caf09 | ||
|
|
a2d853daf5 | ||
|
|
3970619afa | ||
|
|
9dc87761c1 | ||
|
|
86a44fad42 | ||
|
|
91f74144cb | ||
|
|
0863c5170b | ||
|
|
837cd2a463 | ||
|
|
c88a2d5d90 | ||
|
|
c9abc2cb30 | ||
|
|
01824b0b62 | ||
|
|
d1b378992d | ||
|
|
52ca921d2a | ||
|
|
42f12dfef3 | ||
|
|
f2a694447e | ||
|
|
2e7dfa739f | ||
|
|
0aa73580a3 | ||
|
|
2ff1a43bf8 | ||
|
|
c1477c78be | ||
|
|
9807dd5295 | ||
|
|
2c59eeff26 | ||
|
|
8ccfb4efef | ||
|
|
87d18160e8 | ||
|
|
bfa7ee96da | ||
|
|
5e3eb66d3a | ||
|
|
3d8cdf18bd | ||
|
|
cb4e501047 | ||
|
|
cb8b2137ba | ||
|
|
998315a255 | ||
|
|
250657e46b | ||
|
|
795ae9ab18 | ||
|
|
6a9ea8d9f8 | ||
|
|
2723e18023 | ||
|
|
6e89d5f6eb | ||
|
|
4c2a815236 | ||
|
|
b1d66b2e5f | ||
|
|
ae88edbb5e | ||
|
|
7c9484d47b | ||
|
|
24128bd394 | ||
|
|
2118916a23 | ||
|
|
52220412a1 | ||
|
|
85abee8476 | ||
|
|
650a29d184 | ||
|
|
d9c7101d22 | ||
|
|
b1e7c25189 | ||
|
|
e9904a0558 | ||
|
|
5cd199f535 | ||
|
|
f6f48ca0bc | ||
|
|
847f91e22e | ||
|
|
29d0abe5a8 | ||
|
|
c08840a827 | ||
|
|
a3e7bb90b0 | ||
|
|
8515d2f37c | ||
|
|
07c05ac3a6 | ||
|
|
6289f59ba3 | ||
|
|
76371c9fa2 | ||
|
|
f082e396eb | ||
|
|
840eb8f228 | ||
|
|
2911baf6bb | ||
|
|
fc5be4eeb5 | ||
|
|
a1b92c79a4 | ||
|
|
7a0acd5c8b | ||
|
|
069cbe2c6f | ||
|
|
4c821f9721 | ||
|
|
4eccea92db | ||
|
|
c8d8966a5d | ||
|
|
1e52a5603e | ||
|
|
780ba1a359 | ||
|
|
3b71abe820 | ||
|
|
70b9d0ff02 | ||
|
|
f4657861e1 | ||
|
|
66fe5b5240 | ||
|
|
c333cecf43 | ||
|
|
276e09853e | ||
|
|
4defd41504 | ||
|
|
ab53b29a14 | ||
|
|
b58e82efbf | ||
|
|
0a1a676877 | ||
|
|
bb2aa9f77c | ||
|
|
04bef4ac06 | ||
|
|
3bcb2c2c41 | ||
|
|
9e77b76122 | ||
|
|
ff4a41d842 | ||
|
|
387deb779d | ||
|
|
1ec2663d51 | ||
|
|
1b17370da0 | ||
|
|
c6484a79e2 | ||
|
|
16a2c7a1af | ||
|
|
3c4ac0e85e | ||
|
|
87ba729a00 | ||
|
|
f1ed7145e4 | ||
|
|
bc15495e17 | ||
|
|
f7d3012daf | ||
|
|
6ec9a2ec41 | ||
|
|
9c056f809a | ||
|
|
c1d4273416 | ||
|
|
618fe891d5 | ||
|
|
549c7e7034 | ||
|
|
dd65f83c3d | ||
|
|
8463a131fc | ||
|
|
2d42518440 | ||
|
|
43d75a3853 | ||
|
|
c5bb34e385 | ||
|
|
6fd129991d | ||
|
|
9c5cca426a | ||
|
|
a467efb97d | ||
|
|
58e2718090 | ||
|
|
65fee725c9 | ||
|
|
ea87174088 | ||
|
|
627c483d86 | ||
|
|
2533137db4 | ||
|
|
a774f8a4fe | ||
|
|
8487f6cf66 | ||
|
|
6ebe51126e | ||
|
|
ed64d5cd9f | ||
|
|
c04076e664 | ||
|
|
3c129e2c7d | ||
|
|
0ba51e2058 | ||
|
|
cdc2ab134c | ||
|
|
fb0c05b553 | ||
|
|
68e9707e3b | ||
|
|
17ffaf9ccf | ||
|
|
efec669b76 | ||
|
|
17b9e14d34 | ||
|
|
2db9f969c3 | ||
|
|
9fa466b124 | ||
|
|
0c7768ebff | ||
|
|
58dd51e92f | ||
|
|
870c9bf6dc | ||
|
|
7604956bf0 | ||
|
|
66510e4919 | ||
|
|
a1bf0e67db | ||
|
|
a06046612a | ||
|
|
31c9d4309b | ||
|
|
7bef8b86c4 | ||
|
|
d26acd36a3 | ||
|
|
1cee595135 | ||
|
|
dd1868fcbc | ||
|
|
a20beb8ba2 | ||
|
|
998d652feb | ||
|
|
3695d3c180 | ||
|
|
da175bafbc | ||
|
|
021b187cbc | ||
|
|
f42b468597 | ||
|
|
7e2cf57819 | ||
|
|
dc9ebc5b26 | ||
|
|
398ab6e9d9 | ||
|
|
fec60671d8 | ||
|
|
99259cc4e8 | ||
|
|
ca311717c2 | ||
|
|
a614da2c65 | ||
|
|
ce18709002 | ||
|
|
2b6977e891 | ||
|
|
3e6eedbcab | ||
|
|
fd9e3f0411 | ||
|
|
e99465e030 | ||
|
|
9ad2db4b99 | ||
|
|
07fd5f70ef | ||
|
|
ba79121795 | ||
|
|
6e4e419b5e | ||
|
|
2f06afaf27 | ||
|
|
f77c3cb23c | ||
|
|
9e3a8efcfc | ||
|
|
8e325ba8b3 | ||
|
|
884f516766 | ||
|
|
4bcbb4ffc3 |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -107,6 +107,7 @@ go.mod @therealpandey
|
||||
/pkg/modules/organization/ @therealpandey
|
||||
/pkg/modules/authdomain/ @therealpandey
|
||||
/pkg/modules/role/ @therealpandey
|
||||
/pkg/types/coretypes/ @therealpandey @vikrantgupta25
|
||||
|
||||
# IdentN Owners
|
||||
|
||||
|
||||
17
.github/workflows/goci.yaml
vendored
17
.github/workflows/goci.yaml
vendored
@@ -102,3 +102,20 @@ jobs:
|
||||
run: |
|
||||
go run cmd/enterprise/*.go generate openapi
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in openapi spec. Run go run cmd/enterprise/*.go generate openapi locally and commit."; exit 1)
|
||||
authz:
|
||||
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: generate-authz
|
||||
run: |
|
||||
go run cmd/enterprise/*.go generate authz
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in authz permissions. Run go run cmd/enterprise/*.go generate authz locally and commit."; exit 1)
|
||||
|
||||
40
.github/workflows/jsci.yaml
vendored
40
.github/workflows/jsci.yaml
vendored
@@ -63,46 +63,6 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
- name: run
|
||||
run: bash frontend/scripts/validate-md-languages.sh
|
||||
authz:
|
||||
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@v5
|
||||
- name: node-install
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: deps-install
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
yarn install
|
||||
- name: uv-install
|
||||
uses: astral-sh/setup-uv@v5
|
||||
- name: uv-deps
|
||||
working-directory: ./tests/integration
|
||||
run: |
|
||||
uv sync
|
||||
- name: setup-test
|
||||
run: |
|
||||
make py-test-setup
|
||||
- name: generate
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
yarn generate:permissions-type
|
||||
- name: teardown-test
|
||||
if: always()
|
||||
run: |
|
||||
make py-test-teardown
|
||||
- name: validate
|
||||
run: |
|
||||
if ! git diff --exit-code frontend/src/hooks/useAuthZ/permissions.type.ts; then
|
||||
echo "::error::frontend/src/hooks/useAuthZ/permissions.type.ts is out of date. Please run the generator locally and commit the changes: npm run generate:permissions-type (from the frontend directory)"
|
||||
exit 1
|
||||
fi
|
||||
openapi:
|
||||
if: |
|
||||
github.event_name == 'merge_group' ||
|
||||
|
||||
117
cmd/authz.go
Normal file
117
cmd/authz.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"sort"
|
||||
"text/template"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const permissionsTypePath = "frontend/src/hooks/useAuthZ/permissions.type.ts"
|
||||
|
||||
var permissionsTypeTemplate = template.Must(template.New("permissions").Parse(
|
||||
`// AUTO GENERATED FILE - DO NOT EDIT - GENERATED BY cmd/enterprise/*.go generate authz
|
||||
export default {
|
||||
status: 'success',
|
||||
data: {
|
||||
resources: [
|
||||
{{- range .Resources }}
|
||||
{
|
||||
kind: '{{ .Kind }}',
|
||||
type: '{{ .Type }}',
|
||||
},
|
||||
{{- end }}
|
||||
],
|
||||
relations: {
|
||||
{{- range .Relations }}
|
||||
{{ .Verb }}: [{{ range $i, $t := .Types }}{{ if $i }}, {{ end }}'{{ $t }}'{{ end }}],
|
||||
{{- end }}
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
`))
|
||||
|
||||
type permissionsTypeRelation struct {
|
||||
Verb string
|
||||
Types []string
|
||||
}
|
||||
|
||||
type permissionsTypeResource struct {
|
||||
Kind string
|
||||
Type string
|
||||
}
|
||||
|
||||
type permissionsTypeData struct {
|
||||
Resources []permissionsTypeResource
|
||||
Relations []permissionsTypeRelation
|
||||
}
|
||||
|
||||
func registerGenerateAuthz(parentCmd *cobra.Command) {
|
||||
authzCmd := &cobra.Command{
|
||||
Use: "authz",
|
||||
Short: "Generate authz permissions for the frontend",
|
||||
RunE: func(currCmd *cobra.Command, args []string) error {
|
||||
return runGenerateAuthz(currCmd.Context())
|
||||
},
|
||||
}
|
||||
|
||||
parentCmd.AddCommand(authzCmd)
|
||||
}
|
||||
|
||||
func runGenerateAuthz(_ context.Context) error {
|
||||
registry := coretypes.NewRegistry()
|
||||
|
||||
allowedResources := map[string]bool{
|
||||
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourcesRole).String(): true,
|
||||
}
|
||||
|
||||
allowedTypes := map[string]bool{}
|
||||
|
||||
refs := registry.ResourceRefs()
|
||||
resources := make([]permissionsTypeResource, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
if !allowedResources[ref.String()] {
|
||||
continue
|
||||
}
|
||||
allowedTypes[ref.Type.StringValue()] = true
|
||||
resources = append(resources, permissionsTypeResource{
|
||||
Kind: ref.Kind.String(),
|
||||
Type: ref.Type.StringValue(),
|
||||
})
|
||||
}
|
||||
|
||||
typesByVerb := registry.TypesByVerb()
|
||||
verbs := make([]coretypes.Verb, 0, len(typesByVerb))
|
||||
for verb := range typesByVerb {
|
||||
verbs = append(verbs, verb)
|
||||
}
|
||||
sort.Slice(verbs, func(i, j int) bool { return verbs[i].StringValue() < verbs[j].StringValue() })
|
||||
|
||||
relations := make([]permissionsTypeRelation, 0, len(verbs))
|
||||
for _, verb := range verbs {
|
||||
types := make([]string, 0, len(typesByVerb[verb]))
|
||||
for _, t := range typesByVerb[verb] {
|
||||
if !allowedTypes[t.StringValue()] {
|
||||
continue
|
||||
}
|
||||
types = append(types, t.StringValue())
|
||||
}
|
||||
relations = append(relations, permissionsTypeRelation{
|
||||
Verb: verb.StringValue(),
|
||||
Types: types,
|
||||
})
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := permissionsTypeTemplate.Execute(&buf, permissionsTypeData{Resources: resources, Relations: relations}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(permissionsTypePath, buf.Bytes(), 0o600)
|
||||
}
|
||||
@@ -92,13 +92,13 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
|
||||
return signoz.NewAuthNs(ctx, providerSettings, store, licensing)
|
||||
},
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete, _ dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
|
||||
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore)
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore, config authz.Config, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
|
||||
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore), nil
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, authtypes.NewRegistry()), nil
|
||||
},
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
|
||||
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)
|
||||
|
||||
@@ -137,12 +137,12 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
|
||||
return authNs, nil
|
||||
},
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, dashboardModule dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
|
||||
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore)
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore, config authz.Config, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
|
||||
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, dashboardModule), nil
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, authtypes.NewRegistry()), nil
|
||||
},
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
|
||||
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)
|
||||
|
||||
@@ -16,6 +16,7 @@ func RegisterGenerate(parentCmd *cobra.Command, logger *slog.Logger) {
|
||||
}
|
||||
|
||||
registerGenerateOpenAPI(generateCmd)
|
||||
registerGenerateAuthz(generateCmd)
|
||||
|
||||
parentCmd.AddCommand(generateCmd)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ global:
|
||||
ingestion_url: <unset>
|
||||
# the url of the SigNoz MCP server. when unset, the MCP settings page is hidden in the frontend.
|
||||
# mcp_url: <unset>
|
||||
# the url of the SigNoz AI Assistant server. when unset, the AI Assistant is hidden in the frontend.
|
||||
# ai_assistant_url: <unset>
|
||||
|
||||
##################### Version #####################
|
||||
version:
|
||||
@@ -426,4 +428,4 @@ authz:
|
||||
provider: openfga
|
||||
openfga:
|
||||
# maximum tuples allowed per openfga write operation.
|
||||
max_tuples_per_write: 100
|
||||
max_tuples_per_write: 300
|
||||
|
||||
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.121.0
|
||||
image: signoz/signoz:v0.122.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.121.0
|
||||
image: signoz/signoz:v0.122.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
volumes:
|
||||
|
||||
@@ -181,7 +181,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.121.0}
|
||||
image: signoz/signoz:${VERSION:-v0.122.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.121.0}
|
||||
image: signoz/signoz:${VERSION:-v0.122.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -96,6 +96,122 @@ components:
|
||||
- createdAt
|
||||
- updatedAt
|
||||
type: object
|
||||
AlertmanagertypesPostableChannel:
|
||||
oneOf:
|
||||
- required:
|
||||
- discord_configs
|
||||
- required:
|
||||
- email_configs
|
||||
- required:
|
||||
- incidentio_configs
|
||||
- required:
|
||||
- pagerduty_configs
|
||||
- required:
|
||||
- slack_configs
|
||||
- required:
|
||||
- webhook_configs
|
||||
- required:
|
||||
- opsgenie_configs
|
||||
- required:
|
||||
- wechat_configs
|
||||
- required:
|
||||
- pushover_configs
|
||||
- required:
|
||||
- victorops_configs
|
||||
- required:
|
||||
- sns_configs
|
||||
- required:
|
||||
- telegram_configs
|
||||
- required:
|
||||
- webex_configs
|
||||
- required:
|
||||
- msteams_configs
|
||||
- required:
|
||||
- msteamsv2_configs
|
||||
- required:
|
||||
- jira_configs
|
||||
- required:
|
||||
- rocketchat_configs
|
||||
- required:
|
||||
- mattermost_configs
|
||||
properties:
|
||||
discord_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigDiscordConfig'
|
||||
type: array
|
||||
email_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigEmailConfig'
|
||||
type: array
|
||||
incidentio_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigIncidentioConfig'
|
||||
type: array
|
||||
jira_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigJiraConfig'
|
||||
type: array
|
||||
mattermost_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMattermostConfig'
|
||||
type: array
|
||||
msteams_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMSTeamsConfig'
|
||||
type: array
|
||||
msteamsv2_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMSTeamsV2Config'
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
opsgenie_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigOpsGenieConfig'
|
||||
type: array
|
||||
pagerduty_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigPagerdutyConfig'
|
||||
type: array
|
||||
pushover_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigPushoverConfig'
|
||||
type: array
|
||||
rocketchat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigRocketchatConfig'
|
||||
type: array
|
||||
slack_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigSlackConfig'
|
||||
type: array
|
||||
sns_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigSNSConfig'
|
||||
type: array
|
||||
telegram_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigTelegramConfig'
|
||||
type: array
|
||||
victorops_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigVictorOpsConfig'
|
||||
type: array
|
||||
webex_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWebexConfig'
|
||||
type: array
|
||||
webhook_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWebhookConfig'
|
||||
type: array
|
||||
wechat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWechatConfig'
|
||||
type: array
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
AlertmanagertypesPostableRoutePolicy:
|
||||
properties:
|
||||
channels:
|
||||
@@ -133,6 +249,10 @@ components:
|
||||
type: string
|
||||
type: object
|
||||
AuthtypesAuthDomainConfig:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/AuthtypesSamlConfig'
|
||||
- $ref: '#/components/schemas/AuthtypesGoogleConfig'
|
||||
- $ref: '#/components/schemas/AuthtypesOIDCConfig'
|
||||
properties:
|
||||
googleAuthConfig:
|
||||
$ref: '#/components/schemas/AuthtypesGoogleConfig'
|
||||
@@ -145,8 +265,15 @@ components:
|
||||
ssoEnabled:
|
||||
type: boolean
|
||||
ssoType:
|
||||
type: string
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
type: object
|
||||
AuthtypesAuthNProvider:
|
||||
enum:
|
||||
- google_auth
|
||||
- saml
|
||||
- email_password
|
||||
- oidc
|
||||
type: string
|
||||
AuthtypesAuthNProviderInfo:
|
||||
properties:
|
||||
relayStatePath:
|
||||
@@ -169,7 +296,7 @@ components:
|
||||
AuthtypesCallbackAuthNSupport:
|
||||
properties:
|
||||
provider:
|
||||
type: string
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
@@ -177,62 +304,23 @@ components:
|
||||
properties:
|
||||
authNProviderInfo:
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProviderInfo'
|
||||
config:
|
||||
$ref: '#/components/schemas/AuthtypesAuthDomainConfig'
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
googleAuthConfig:
|
||||
$ref: '#/components/schemas/AuthtypesGoogleConfig'
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
oidcConfig:
|
||||
$ref: '#/components/schemas/AuthtypesOIDCConfig'
|
||||
orgId:
|
||||
type: string
|
||||
roleMapping:
|
||||
$ref: '#/components/schemas/AuthtypesRoleMapping'
|
||||
samlConfig:
|
||||
$ref: '#/components/schemas/AuthtypesSamlConfig'
|
||||
ssoEnabled:
|
||||
type: boolean
|
||||
ssoType:
|
||||
type: string
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
AuthtypesGettableObjects:
|
||||
properties:
|
||||
resource:
|
||||
$ref: '#/components/schemas/AuthtypesResource'
|
||||
selectors:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- resource
|
||||
- selectors
|
||||
type: object
|
||||
AuthtypesGettableResources:
|
||||
properties:
|
||||
relations:
|
||||
additionalProperties:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
nullable: true
|
||||
type: object
|
||||
resources:
|
||||
items:
|
||||
$ref: '#/components/schemas/AuthtypesResource'
|
||||
type: array
|
||||
required:
|
||||
- resources
|
||||
- relations
|
||||
type: object
|
||||
AuthtypesGettableToken:
|
||||
properties:
|
||||
accessToken:
|
||||
@@ -249,9 +337,9 @@ components:
|
||||
authorized:
|
||||
type: boolean
|
||||
object:
|
||||
$ref: '#/components/schemas/AuthtypesObject'
|
||||
$ref: '#/components/schemas/CoretypesObject'
|
||||
relation:
|
||||
type: string
|
||||
$ref: '#/components/schemas/AuthtypesRelation'
|
||||
required:
|
||||
- relation
|
||||
- object
|
||||
@@ -299,16 +387,6 @@ components:
|
||||
issuerAlias:
|
||||
type: string
|
||||
type: object
|
||||
AuthtypesObject:
|
||||
properties:
|
||||
resource:
|
||||
$ref: '#/components/schemas/AuthtypesResource'
|
||||
selector:
|
||||
type: string
|
||||
required:
|
||||
- resource
|
||||
- selector
|
||||
type: object
|
||||
AuthtypesOrgSessionContext:
|
||||
properties:
|
||||
authNSupport:
|
||||
@@ -323,23 +401,7 @@ components:
|
||||
AuthtypesPasswordAuthNSupport:
|
||||
properties:
|
||||
provider:
|
||||
type: string
|
||||
type: object
|
||||
AuthtypesPatchableObjects:
|
||||
properties:
|
||||
additions:
|
||||
items:
|
||||
$ref: '#/components/schemas/AuthtypesGettableObjects'
|
||||
nullable: true
|
||||
type: array
|
||||
deletions:
|
||||
items:
|
||||
$ref: '#/components/schemas/AuthtypesGettableObjects'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- additions
|
||||
- deletions
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
type: object
|
||||
AuthtypesPatchableRole:
|
||||
properties:
|
||||
@@ -378,16 +440,15 @@ components:
|
||||
refreshToken:
|
||||
type: string
|
||||
type: object
|
||||
AuthtypesResource:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- type
|
||||
type: object
|
||||
AuthtypesRelation:
|
||||
enum:
|
||||
- create
|
||||
- read
|
||||
- update
|
||||
- delete
|
||||
- list
|
||||
- assignee
|
||||
type: string
|
||||
AuthtypesRole:
|
||||
properties:
|
||||
createdAt:
|
||||
@@ -451,14 +512,14 @@ components:
|
||||
AuthtypesTransaction:
|
||||
properties:
|
||||
object:
|
||||
$ref: '#/components/schemas/AuthtypesObject'
|
||||
$ref: '#/components/schemas/CoretypesObject'
|
||||
relation:
|
||||
type: string
|
||||
$ref: '#/components/schemas/AuthtypesRelation'
|
||||
required:
|
||||
- relation
|
||||
- object
|
||||
type: object
|
||||
AuthtypesUpdateableAuthDomain:
|
||||
AuthtypesUpdatableAuthDomain:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/components/schemas/AuthtypesAuthDomainConfig'
|
||||
@@ -2088,6 +2149,64 @@ components:
|
||||
to_user:
|
||||
type: string
|
||||
type: object
|
||||
CoretypesObject:
|
||||
properties:
|
||||
resource:
|
||||
$ref: '#/components/schemas/CoretypesResourceRef'
|
||||
selector:
|
||||
type: string
|
||||
required:
|
||||
- resource
|
||||
- selector
|
||||
type: object
|
||||
CoretypesObjectGroup:
|
||||
properties:
|
||||
resource:
|
||||
$ref: '#/components/schemas/CoretypesResourceRef'
|
||||
selectors:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- resource
|
||||
- selectors
|
||||
type: object
|
||||
CoretypesPatchableObjects:
|
||||
properties:
|
||||
additions:
|
||||
items:
|
||||
$ref: '#/components/schemas/CoretypesObjectGroup'
|
||||
nullable: true
|
||||
type: array
|
||||
deletions:
|
||||
items:
|
||||
$ref: '#/components/schemas/CoretypesObjectGroup'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- additions
|
||||
- deletions
|
||||
type: object
|
||||
CoretypesResourceRef:
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
type:
|
||||
$ref: '#/components/schemas/CoretypesType'
|
||||
required:
|
||||
- type
|
||||
- kind
|
||||
type: object
|
||||
CoretypesType:
|
||||
enum:
|
||||
- user
|
||||
- serviceaccount
|
||||
- anonymous
|
||||
- role
|
||||
- organization
|
||||
- metaresource
|
||||
- metaresources
|
||||
type: string
|
||||
DashboardtypesDashboard:
|
||||
properties:
|
||||
createdAt:
|
||||
@@ -2363,6 +2482,9 @@ components:
|
||||
type: object
|
||||
GlobaltypesConfig:
|
||||
properties:
|
||||
ai_assistant_url:
|
||||
nullable: true
|
||||
type: string
|
||||
external_url:
|
||||
type: string
|
||||
identN:
|
||||
@@ -2376,6 +2498,7 @@ components:
|
||||
- external_url
|
||||
- ingestion_url
|
||||
- mcp_url
|
||||
- ai_assistant_url
|
||||
type: object
|
||||
GlobaltypesIdentNConfig:
|
||||
properties:
|
||||
@@ -2632,6 +2755,158 @@ components:
|
||||
- list
|
||||
- grouped_list
|
||||
type: string
|
||||
LlmpricingruletypesGettablePricingRules:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRule'
|
||||
nullable: true
|
||||
type: array
|
||||
limit:
|
||||
type: integer
|
||||
offset:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
required:
|
||||
- items
|
||||
- total
|
||||
- offset
|
||||
- limit
|
||||
type: object
|
||||
LlmpricingruletypesLLMPricingCacheCosts:
|
||||
properties:
|
||||
mode:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleCacheMode'
|
||||
read:
|
||||
format: double
|
||||
type: number
|
||||
write:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- mode
|
||||
type: object
|
||||
LlmpricingruletypesLLMPricingRule:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
id:
|
||||
type: string
|
||||
isOverride:
|
||||
type: boolean
|
||||
modelName:
|
||||
type: string
|
||||
modelPattern:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesStringSlice'
|
||||
orgId:
|
||||
type: string
|
||||
pricing:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMRulePricing'
|
||||
provider:
|
||||
type: string
|
||||
sourceId:
|
||||
type: string
|
||||
syncedAt:
|
||||
format: date-time
|
||||
nullable: true
|
||||
type: string
|
||||
unit:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleUnit'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- orgId
|
||||
- modelName
|
||||
- provider
|
||||
- modelPattern
|
||||
- unit
|
||||
- pricing
|
||||
- isOverride
|
||||
- enabled
|
||||
type: object
|
||||
LlmpricingruletypesLLMPricingRuleCacheMode:
|
||||
enum:
|
||||
- subtract
|
||||
- additive
|
||||
- unknown
|
||||
type: string
|
||||
LlmpricingruletypesLLMPricingRuleUnit:
|
||||
enum:
|
||||
- per_million_tokens
|
||||
type: string
|
||||
LlmpricingruletypesLLMRulePricing:
|
||||
properties:
|
||||
cache:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingCacheCosts'
|
||||
input:
|
||||
format: double
|
||||
type: number
|
||||
output:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- input
|
||||
- output
|
||||
type: object
|
||||
LlmpricingruletypesStringSlice:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
LlmpricingruletypesUpdatableLLMPricingRule:
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
id:
|
||||
nullable: true
|
||||
type: string
|
||||
isOverride:
|
||||
nullable: true
|
||||
type: boolean
|
||||
modelName:
|
||||
type: string
|
||||
modelPattern:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
pricing:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMRulePricing'
|
||||
provider:
|
||||
type: string
|
||||
sourceId:
|
||||
nullable: true
|
||||
type: string
|
||||
unit:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleUnit'
|
||||
required:
|
||||
- modelName
|
||||
- provider
|
||||
- modelPattern
|
||||
- unit
|
||||
- pricing
|
||||
- enabled
|
||||
type: object
|
||||
LlmpricingruletypesUpdatableLLMPricingRules:
|
||||
properties:
|
||||
rules:
|
||||
items:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesUpdatableLLMPricingRule'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- rules
|
||||
type: object
|
||||
MetricsexplorertypesInspectMetricsRequest:
|
||||
properties:
|
||||
end:
|
||||
@@ -5048,6 +5323,9 @@ components:
|
||||
sub_tree_node_count:
|
||||
minimum: 0
|
||||
type: integer
|
||||
time_unix:
|
||||
minimum: 0
|
||||
type: integer
|
||||
trace_id:
|
||||
type: string
|
||||
trace_state:
|
||||
@@ -5428,35 +5706,6 @@ paths:
|
||||
summary: Check permissions
|
||||
tags:
|
||||
- authz
|
||||
/api/v1/authz/resources:
|
||||
get:
|
||||
deprecated: false
|
||||
description: Gets all the available resources
|
||||
operationId: AuthzResources
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AuthtypesGettableResources'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
summary: Get resources
|
||||
tags:
|
||||
- authz
|
||||
/api/v1/channels:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -5513,7 +5762,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ConfigReceiver'
|
||||
$ref: '#/components/schemas/AlertmanagertypesPostableChannel'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
@@ -6792,20 +7041,20 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthtypesPostableAuthDomain'
|
||||
responses:
|
||||
"200":
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AuthtypesGettableAuthDomain'
|
||||
$ref: '#/components/schemas/TypesIdentifiable'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
description: Created
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
@@ -6890,6 +7139,63 @@ paths:
|
||||
summary: Delete auth domain
|
||||
tags:
|
||||
- authdomains
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns an auth domain by ID
|
||||
operationId: GetAuthDomain
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AuthtypesGettableAuthDomain'
|
||||
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 auth domain by ID
|
||||
tags:
|
||||
- authdomains
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint updates an auth domain
|
||||
@@ -6904,7 +7210,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthtypesUpdateableAuthDomain'
|
||||
$ref: '#/components/schemas/AuthtypesUpdatableAuthDomain'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
@@ -7675,6 +7981,218 @@ paths:
|
||||
summary: Create bulk invite
|
||||
tags:
|
||||
- users
|
||||
/api/v1/llm_pricing_rules:
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns all LLM pricing rules for the authenticated org, with pagination.
|
||||
operationId: ListLLMPricingRules
|
||||
parameters:
|
||||
- in: query
|
||||
name: offset
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesGettablePricingRules'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: List pricing rules
|
||||
tags:
|
||||
- llmpricingrules
|
||||
put:
|
||||
deprecated: false
|
||||
description: Single write endpoint used by both the user and the Zeus sync job.
|
||||
Per-rule match is by id, then sourceId, then insert. Override rows (is_override=true)
|
||||
are fully preserved when the request does not provide isOverride; only synced_at
|
||||
is stamped.
|
||||
operationId: CreateOrUpdateLLMPricingRules
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesUpdatableLLMPricingRules'
|
||||
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
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Create or update pricing rules
|
||||
tags:
|
||||
- llmpricingrules
|
||||
/api/v1/llm_pricing_rules/{id}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: Hard-deletes a pricing rule. If auto-synced, it will be recreated
|
||||
on the next sync cycle.
|
||||
operationId: DeleteLLMPricingRule
|
||||
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 a pricing rule
|
||||
tags:
|
||||
- llmpricingrules
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns a single LLM pricing rule by ID.
|
||||
operationId: GetLLMPricingRule
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRule'
|
||||
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:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Get a pricing rule
|
||||
tags:
|
||||
- llmpricingrules
|
||||
/api/v1/logs/promote_paths:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -8411,7 +8929,7 @@ paths:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/components/schemas/AuthtypesGettableObjects'
|
||||
$ref: '#/components/schemas/CoretypesObjectGroup'
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
@@ -8484,7 +9002,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthtypesPatchableObjects'
|
||||
$ref: '#/components/schemas/CoretypesPatchableObjects'
|
||||
responses:
|
||||
"204":
|
||||
content:
|
||||
@@ -16909,9 +17427,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- VIEWER
|
||||
summary: Get host info from Zeus.
|
||||
tags:
|
||||
- zeus
|
||||
|
||||
@@ -2,7 +2,6 @@ package openfgaauthz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/authz/openfgaserver"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer"
|
||||
@@ -25,19 +25,19 @@ type provider struct {
|
||||
openfgaServer *openfgaserver.Server
|
||||
licensing licensing.Licensing
|
||||
store authtypes.RoleStore
|
||||
registry []authz.RegisterTypeable
|
||||
registry *authtypes.Registry
|
||||
settings factory.ScopedProviderSettings
|
||||
onBeforeRoleDelete []authz.OnBeforeRoleDelete
|
||||
}
|
||||
|
||||
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry ...authz.RegisterTypeable) factory.ProviderFactory[authz.AuthZ, authz.Config] {
|
||||
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry *authtypes.Registry) factory.ProviderFactory[authz.AuthZ, authz.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("openfga"), func(ctx context.Context, ps factory.ProviderSettings, config authz.Config) (authz.AuthZ, error) {
|
||||
return newOpenfgaProvider(ctx, ps, config, sqlstore, openfgaSchema, openfgaDataStore, licensing, onBeforeRoleDelete, registry)
|
||||
})
|
||||
}
|
||||
|
||||
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry []authz.RegisterTypeable) (authz.AuthZ, error) {
|
||||
pkgOpenfgaAuthzProvider := pkgopenfgaauthz.NewProviderFactory(sqlstore, openfgaSchema, openfgaDataStore)
|
||||
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry *authtypes.Registry) (authz.AuthZ, error) {
|
||||
pkgOpenfgaAuthzProvider := pkgopenfgaauthz.NewProviderFactory(sqlstore, openfgaSchema, openfgaDataStore, registry)
|
||||
pkgAuthzService, err := pkgOpenfgaAuthzProvider.New(ctx, settings, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -74,11 +74,11 @@ func (provider *provider) Stop(ctx context.Context) error {
|
||||
return provider.openfgaServer.Stop(ctx)
|
||||
}
|
||||
|
||||
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, roleSelectors []authtypes.Selector) error {
|
||||
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable coretypes.Resource, selectors []coretypes.Selector, roleSelectors []coretypes.Selector) error {
|
||||
return provider.openfgaServer.CheckWithTupleCreation(ctx, claims, orgID, relation, typeable, selectors, roleSelectors)
|
||||
}
|
||||
|
||||
func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, roleSelectors []authtypes.Selector) error {
|
||||
func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable coretypes.Resource, selectors []coretypes.Selector, roleSelectors []coretypes.Selector) error {
|
||||
return provider.openfgaServer.CheckWithTupleCreationWithoutClaims(ctx, orgID, relation, typeable, selectors, roleSelectors)
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ func (provider *provider) CheckTransactions(ctx context.Context, subject string,
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType authtypes.Type) ([]*authtypes.Object, error) {
|
||||
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
|
||||
return provider.openfgaServer.ListObjects(ctx, subject, relation, objectType)
|
||||
}
|
||||
|
||||
@@ -159,16 +159,10 @@ func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.U
|
||||
func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error {
|
||||
tuples := make([]*openfgav1.TupleKey, 0)
|
||||
|
||||
grantTuples, err := provider.getManagedRoleGrantTuples(orgID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
grantTuples := provider.getManagedRoleGrantTuples(orgID, userID)
|
||||
tuples = append(tuples, grantTuples...)
|
||||
|
||||
managedRoleTuples, err := provider.getManagedRoleTransactionTuples(orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
managedRoleTuples := provider.getManagedRoleTransactionTuples(orgID)
|
||||
tuples = append(tuples, managedRoleTuples...)
|
||||
|
||||
return provider.Write(ctx, tuples, nil)
|
||||
@@ -208,21 +202,7 @@ func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, ro
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func (provider *provider) GetResources(_ context.Context) []*authtypes.Resource {
|
||||
resources := make([]*authtypes.Resource, 0)
|
||||
for _, register := range provider.registry {
|
||||
for _, typeable := range register.MustGetTypeables() {
|
||||
resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()})
|
||||
}
|
||||
}
|
||||
for _, typeable := range provider.MustGetTypeables() {
|
||||
resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()})
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) {
|
||||
func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*coretypes.Object, error) {
|
||||
_, err := provider.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
@@ -233,16 +213,16 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objects := make([]*authtypes.Object, 0)
|
||||
for _, objectType := range provider.getUniqueTypes() {
|
||||
if !slices.Contains(authtypes.TypeableRelations[objectType], relation) {
|
||||
objects := make([]*coretypes.Object, 0)
|
||||
for _, objectType := range provider.registry.Types() {
|
||||
if coretypes.ErrIfVerbNotValidForType(relation.Verb, objectType) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
resourceObjects, err := provider.
|
||||
ListObjects(
|
||||
ctx,
|
||||
authtypes.MustNewSubject(authtypes.TypeableRole, storableRole.Name, orgID, &authtypes.RelationAssignee),
|
||||
authtypes.MustNewSubject(coretypes.NewResourceRole(), storableRole.Name, orgID, &coretypes.VerbAssignee),
|
||||
relation,
|
||||
objectType,
|
||||
)
|
||||
@@ -265,7 +245,7 @@ func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *au
|
||||
return provider.store.Update(ctx, orgID, role)
|
||||
}
|
||||
|
||||
func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
|
||||
func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*coretypes.Object) error {
|
||||
_, err := provider.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
@@ -318,84 +298,63 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
|
||||
return provider.store.Delete(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (provider *provider) MustGetTypeables() []authtypes.Typeable {
|
||||
return []authtypes.Typeable{authtypes.TypeableRole, authtypes.TypeableResourcesRoles}
|
||||
}
|
||||
|
||||
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) ([]*openfgav1.TupleKey, error) {
|
||||
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) []*openfgav1.TupleKey {
|
||||
tuples := []*openfgav1.TupleKey{}
|
||||
|
||||
// Grant the admin role to the user
|
||||
adminSubject := authtypes.MustNewSubject(authtypes.TypeableUser, userID.String(), orgID, nil)
|
||||
adminTuple, err := authtypes.TypeableRole.Tuples(
|
||||
adminSubject := authtypes.MustNewSubject(coretypes.NewResourceUser(), userID.String(), orgID, nil)
|
||||
adminTuple := authtypes.NewTuples(
|
||||
coretypes.NewResourceRole(),
|
||||
adminSubject,
|
||||
authtypes.RelationAssignee,
|
||||
[]authtypes.Selector{
|
||||
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
|
||||
},
|
||||
authtypes.Relation{Verb: coretypes.VerbAssignee},
|
||||
[]coretypes.Selector{coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName)},
|
||||
orgID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tuples = append(tuples, adminTuple...)
|
||||
|
||||
// Grant the admin role to the anonymous user
|
||||
anonymousSubject := authtypes.MustNewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.String(), orgID, nil)
|
||||
anonymousTuple, err := authtypes.TypeableRole.Tuples(
|
||||
anonymousSubject := authtypes.MustNewSubject(coretypes.NewResourceAnonymous(), coretypes.AnonymousUser.String(), orgID, nil)
|
||||
anonymousTuple := authtypes.NewTuples(
|
||||
coretypes.NewResourceRole(),
|
||||
anonymousSubject,
|
||||
authtypes.RelationAssignee,
|
||||
[]authtypes.Selector{
|
||||
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAnonymousRoleName),
|
||||
},
|
||||
authtypes.Relation{Verb: coretypes.VerbAssignee},
|
||||
[]coretypes.Selector{coretypes.TypeRole.MustSelector(authtypes.SigNozAnonymousRoleName)},
|
||||
orgID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tuples = append(tuples, anonymousTuple...)
|
||||
|
||||
return tuples, nil
|
||||
return tuples
|
||||
}
|
||||
|
||||
func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
|
||||
transactionsByRole := make(map[string][]*authtypes.Transaction)
|
||||
for _, register := range provider.registry {
|
||||
for roleName, txns := range register.MustGetManagedRoleTransactions() {
|
||||
transactionsByRole[roleName] = append(transactionsByRole[roleName], txns...)
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) []*openfgav1.TupleKey {
|
||||
tuples := make([]*openfgav1.TupleKey, 0)
|
||||
for roleName, transactions := range transactionsByRole {
|
||||
for roleName, transactions := range provider.registry.ManagedRoleTransactions() {
|
||||
for _, txn := range transactions {
|
||||
typeable := authtypes.MustNewTypeableFromType(txn.Object.Resource.Type, txn.Object.Resource.Name)
|
||||
txnTuples, err := typeable.Tuples(
|
||||
resource := coretypes.MustNewResourceFromTypeAndKind(txn.Object.Resource.Type, txn.Object.Resource.Kind)
|
||||
txnTuples := authtypes.NewTuples(
|
||||
resource,
|
||||
authtypes.MustNewSubject(
|
||||
authtypes.TypeableRole,
|
||||
coretypes.NewResourceRole(),
|
||||
roleName,
|
||||
orgID,
|
||||
&authtypes.RelationAssignee,
|
||||
&coretypes.VerbAssignee,
|
||||
),
|
||||
txn.Relation,
|
||||
[]authtypes.Selector{txn.Object.Selector},
|
||||
[]coretypes.Selector{txn.Object.Selector},
|
||||
orgID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tuples = append(tuples, txnTuples...)
|
||||
}
|
||||
}
|
||||
|
||||
return tuples, nil
|
||||
return tuples
|
||||
}
|
||||
|
||||
func (provider *provider) deleteTuples(ctx context.Context, roleName string, orgID valuer.UUID) error {
|
||||
subject := authtypes.MustNewSubject(authtypes.TypeableRole, roleName, orgID, &authtypes.RelationAssignee)
|
||||
subject := authtypes.MustNewSubject(coretypes.NewResourceRole(), roleName, orgID, &coretypes.VerbAssignee)
|
||||
|
||||
tuples := make([]*openfgav1.TupleKey, 0)
|
||||
for _, objectType := range provider.getUniqueTypes() {
|
||||
for _, objectType := range provider.registry.Types() {
|
||||
typeTuples, err := provider.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
|
||||
User: subject,
|
||||
Object: objectType.StringValue() + ":",
|
||||
@@ -424,28 +383,3 @@ func (provider *provider) deleteTuples(ctx context.Context, roleName string, org
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) getUniqueTypes() []authtypes.Type {
|
||||
seen := make(map[string]struct{})
|
||||
uniqueTypes := make([]authtypes.Type, 0)
|
||||
for _, register := range provider.registry {
|
||||
for _, typeable := range register.MustGetTypeables() {
|
||||
typeKey := typeable.Type().StringValue()
|
||||
if _, ok := seen[typeKey]; ok {
|
||||
continue
|
||||
}
|
||||
seen[typeKey] = struct{}{}
|
||||
uniqueTypes = append(uniqueTypes, typeable.Type())
|
||||
}
|
||||
}
|
||||
for _, typeable := range provider.MustGetTypeables() {
|
||||
typeKey := typeable.Type().StringValue()
|
||||
if _, ok := seen[typeKey]; ok {
|
||||
continue
|
||||
}
|
||||
seen[typeKey] = struct{}{}
|
||||
uniqueTypes = append(uniqueTypes, typeable.Type())
|
||||
}
|
||||
|
||||
return uniqueTypes
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module base
|
||||
|
||||
type organisation
|
||||
type organization
|
||||
relations
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
define update: [user, serviceaccount, role#assignee]
|
||||
@@ -10,12 +10,14 @@ type user
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
define update: [user, serviceaccount, role#assignee]
|
||||
define delete: [user, serviceaccount, role#assignee]
|
||||
define attach: [user, serviceaccount, role#assignee]
|
||||
|
||||
type serviceaccount
|
||||
type serviceaccount
|
||||
relations
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
define update: [user, serviceaccount, role#assignee]
|
||||
define delete: [user, serviceaccount, role#assignee]
|
||||
define delete: [user, serviceaccount, role#assignee]
|
||||
define attach: [user, serviceaccount, role#assignee]
|
||||
|
||||
type anonymous
|
||||
|
||||
@@ -26,6 +28,7 @@ type role
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
define update: [user, serviceaccount, role#assignee]
|
||||
define delete: [user, serviceaccount, role#assignee]
|
||||
define attach: [user, serviceaccount, role#assignee]
|
||||
|
||||
type metaresources
|
||||
relations
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
)
|
||||
@@ -33,18 +34,18 @@ func (server *Server) Stop(ctx context.Context) error {
|
||||
return server.pkgAuthzService.Stop(ctx)
|
||||
}
|
||||
|
||||
func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, _ []authtypes.Selector) error {
|
||||
func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable coretypes.Resource, selectors []coretypes.Selector, _ []coretypes.Selector) error {
|
||||
subject := ""
|
||||
switch claims.Principal {
|
||||
case authtypes.PrincipalUser:
|
||||
user, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
|
||||
user, err := authtypes.NewSubject(coretypes.NewResourceUser(), claims.UserID, orgID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subject = user
|
||||
case authtypes.PrincipalServiceAccount:
|
||||
serviceAccount, err := authtypes.NewSubject(authtypes.TypeableServiceAccount, claims.ServiceAccountID, orgID, nil)
|
||||
serviceAccount, err := authtypes.NewSubject(coretypes.NewResourceServiceAccount(), claims.ServiceAccountID, orgID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -52,10 +53,7 @@ func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtyp
|
||||
subject = serviceAccount
|
||||
}
|
||||
|
||||
tupleSlice, err := typeable.Tuples(subject, relation, selectors, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tupleSlice := authtypes.NewTuples(typeable, subject, relation, selectors, orgID)
|
||||
|
||||
tuples := make(map[string]*openfgav1.TupleKey, len(tupleSlice))
|
||||
for idx, tuple := range tupleSlice {
|
||||
@@ -76,16 +74,13 @@ func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtyp
|
||||
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "subjects are not authorized for requested access")
|
||||
}
|
||||
|
||||
func (server *Server) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, _ []authtypes.Selector) error {
|
||||
subject, err := authtypes.NewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.String(), orgID, nil)
|
||||
func (server *Server) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable coretypes.Resource, selectors []coretypes.Selector, _ []coretypes.Selector) error {
|
||||
subject, err := authtypes.NewSubject(coretypes.NewResourceAnonymous(), coretypes.AnonymousUser.String(), orgID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tupleSlice, err := typeable.Tuples(subject, relation, selectors, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tupleSlice := authtypes.NewTuples(typeable, subject, relation, selectors, orgID)
|
||||
|
||||
tuples := make(map[string]*openfgav1.TupleKey, len(tupleSlice))
|
||||
for idx, tuple := range tupleSlice {
|
||||
@@ -110,7 +105,7 @@ func (server *Server) BatchCheck(ctx context.Context, tupleReq map[string]*openf
|
||||
return server.pkgAuthzService.BatchCheck(ctx, tupleReq)
|
||||
}
|
||||
|
||||
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType authtypes.Type) ([]*authtypes.Object, error) {
|
||||
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
|
||||
return server.pkgAuthzService.ListObjects(ctx, subject, relation, objectType)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package openfgaserver
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/openfga/openfga/pkg/storage"
|
||||
@@ -10,11 +11,11 @@ import (
|
||||
"github.com/openfga/openfga/pkg/storage/sqlite"
|
||||
)
|
||||
|
||||
func NewSQLStore(store sqlstore.SQLStore) (storage.OpenFGADatastore, error) {
|
||||
func NewSQLStore(store sqlstore.SQLStore, config authz.Config) (storage.OpenFGADatastore, error) {
|
||||
switch store.BunDB().Dialect().Name().String() {
|
||||
case "sqlite":
|
||||
return sqlite.NewWithDB(store.SQLDB(), &sqlcommon.Config{
|
||||
MaxTuplesPerWriteField: 100,
|
||||
MaxTuplesPerWriteField: config.OpenFGA.MaxTuplesPerWrite,
|
||||
MaxTypesPerModelField: 100,
|
||||
})
|
||||
case "pg":
|
||||
@@ -24,7 +25,7 @@ func NewSQLStore(store sqlstore.SQLStore) (storage.OpenFGADatastore, error) {
|
||||
}
|
||||
|
||||
return postgres.NewWithDB(pgStore.Pool(), nil, &sqlcommon.Config{
|
||||
MaxTuplesPerWriteField: 100,
|
||||
MaxTuplesPerWriteField: config.OpenFGA.MaxTuplesPerWrite,
|
||||
MaxTypesPerModelField: 100,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"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/ctxtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
|
||||
@@ -88,7 +88,7 @@ func (module *module) GetDashboardByPublicID(ctx context.Context, id valuer.UUID
|
||||
return dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard), nil
|
||||
}
|
||||
|
||||
func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id valuer.UUID, orgs []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) {
|
||||
func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id valuer.UUID, orgs []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
|
||||
orgIDs := make([]string, len(orgs))
|
||||
for idx, org := range orgs {
|
||||
orgIDs[idx] = org.ID.StringValue()
|
||||
@@ -99,9 +99,9 @@ func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id
|
||||
return nil, valuer.UUID{}, err
|
||||
}
|
||||
|
||||
return []authtypes.Selector{
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, id.StringValue()),
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, authtypes.WildCardSelectorString),
|
||||
return []coretypes.Selector{
|
||||
coretypes.TypeMetaResource.MustSelector(id.StringValue()),
|
||||
coretypes.TypeMetaResource.MustSelector(coretypes.WildCardSelectorString),
|
||||
}, storableDashboard.OrgID, nil
|
||||
}
|
||||
|
||||
@@ -217,28 +217,6 @@ func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valu
|
||||
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, isAdmin, lock)
|
||||
}
|
||||
|
||||
func (module *module) MustGetTypeables() []authtypes.Typeable {
|
||||
return module.pkgDashboardModule.MustGetTypeables()
|
||||
}
|
||||
|
||||
func (module *module) MustGetManagedRoleTransactions() map[string][]*authtypes.Transaction {
|
||||
return map[string][]*authtypes.Transaction{
|
||||
authtypes.SigNozAnonymousRoleName: {
|
||||
{
|
||||
ID: valuer.GenerateUUID(),
|
||||
Relation: authtypes.RelationRead,
|
||||
Object: *authtypes.MustNewObject(
|
||||
authtypes.Resource{
|
||||
Type: authtypes.TypeMetaResource,
|
||||
Name: dashboardtypes.TypeableMetaResourcePublicDashboard.Name(),
|
||||
},
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, "*"),
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (module *module) deletePublic(ctx context.Context, _ valuer.UUID, dashboardID valuer.UUID) error {
|
||||
return module.store.DeletePublic(ctx, dashboardID.StringValue())
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"log/slog"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/anomaly"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
@@ -15,7 +17,6 @@ import (
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -137,4 +138,3 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
|
||||
aH.QueryRangeV4(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,8 +42,8 @@ import (
|
||||
|
||||
// Server runs HTTP, Mux and a grpc server
|
||||
type Server struct {
|
||||
config signoz.Config
|
||||
signoz *signoz.SigNoz
|
||||
config signoz.Config
|
||||
signoz *signoz.SigNoz
|
||||
|
||||
// public http router
|
||||
httpConn net.Listener
|
||||
@@ -148,7 +148,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
|
||||
s := &Server{
|
||||
config: config,
|
||||
signoz: signoz,
|
||||
signoz: signoz,
|
||||
httpHostPort: baseconst.HTTPHostPort,
|
||||
unavailableChannel: make(chan healthcheck.Status),
|
||||
usageManager: usageManager,
|
||||
@@ -317,4 +317,3 @@ func (s *Server) Stop(ctx context.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
"**/*.md",
|
||||
"**/*.json",
|
||||
"src/parser/**",
|
||||
"src/TraceOperator/parser/**"
|
||||
"src/TraceOperator/parser/**",
|
||||
".claude",
|
||||
".opencode",
|
||||
"dist",
|
||||
"playwright-report",
|
||||
".temp_cache"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"project": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"ignore": ["src/api/generated/**/*.ts"]
|
||||
"ignore": ["src/api/generated/**/*.ts", "src/typings/*.ts"]
|
||||
}
|
||||
@@ -23,8 +23,7 @@
|
||||
"commitlint": "commitlint --edit $1",
|
||||
"test": "jest",
|
||||
"test:changedsince": "jest --changedSince=main --coverage --silent",
|
||||
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
|
||||
"generate:permissions-type": "node scripts/generate-permissions-type.cjs"
|
||||
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
@@ -51,7 +50,7 @@
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/icons": "0.1.0",
|
||||
"@signozhq/resizable": "0.0.2",
|
||||
"@signozhq/ui": "0.0.10",
|
||||
"@signozhq/ui": "0.0.12",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
@@ -121,10 +120,12 @@
|
||||
"react-helmet-async": "1.3.0",
|
||||
"react-hook-form": "7.71.2",
|
||||
"react-i18next": "^11.16.1",
|
||||
"react-json-tree": "^0.20.0",
|
||||
"react-lottie": "1.2.10",
|
||||
"react-markdown": "8.0.7",
|
||||
"react-query": "3.39.3",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-rnd": "^10.5.3",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-router-dom-v5-compat": "6.27.0",
|
||||
"react-syntax-highlighter": "15.5.0",
|
||||
@@ -198,6 +199,7 @@
|
||||
"@types/redux-mock-store": "1.0.4",
|
||||
"@types/styled-components": "^5.1.4",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260421.2",
|
||||
"autoprefixer": "10.4.19",
|
||||
"babel-plugin-styled-components": "^1.12.0",
|
||||
"eslint-plugin-sonarjs": "4.0.2",
|
||||
@@ -231,6 +233,7 @@
|
||||
"ts-jest": "29.4.6",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript-plugin-css-modules": "5.2.0",
|
||||
"use-sync-external-store": "1.6.0",
|
||||
"vite-plugin-checker": "0.12.0",
|
||||
"vite-plugin-compression": "0.5.1",
|
||||
"vite-plugin-image-optimizer": "2.0.3",
|
||||
@@ -240,7 +243,7 @@
|
||||
"*.(js|jsx|ts|tsx)": [
|
||||
"oxlint --fix",
|
||||
"oxfmt --write",
|
||||
"sh scripts/typecheck-staged.sh"
|
||||
"sh -c tsgo --noEmit"
|
||||
],
|
||||
"*.(scss|css)": [
|
||||
"stylelint"
|
||||
@@ -266,4 +269,4 @@
|
||||
"tmp": "0.2.4",
|
||||
"vite": "npm:rolldown-vite@7.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const axios = require('axios');
|
||||
|
||||
const PERMISSIONS_TYPE_FILE = path.join(
|
||||
__dirname,
|
||||
'../src/hooks/useAuthZ/permissions.type.ts',
|
||||
);
|
||||
|
||||
const SIGNOZ_INTEGRATION_IMAGE = 'signoz:integration';
|
||||
const LOCAL_BACKEND_URL = 'http://localhost:8080';
|
||||
|
||||
function log(message) {
|
||||
console.log(`[generate-permissions-type] ${message}`);
|
||||
}
|
||||
|
||||
function getBackendUrlFromDocker() {
|
||||
try {
|
||||
const output = execSync(
|
||||
`docker ps --filter "ancestor=${SIGNOZ_INTEGRATION_IMAGE}" --format "{{.Ports}}"`,
|
||||
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
|
||||
).trim();
|
||||
|
||||
if (!output) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const portMatch = output.match(/0\.0\.0\.0:(\d+)->8080\/tcp/);
|
||||
if (portMatch) {
|
||||
return `http://localhost:${portMatch[1]}`;
|
||||
}
|
||||
|
||||
const ipv6Match = output.match(/:::(\d+)->8080\/tcp/);
|
||||
if (ipv6Match) {
|
||||
return `http://localhost:${ipv6Match[1]}`;
|
||||
}
|
||||
} catch (err) {
|
||||
log(`Warning: Could not get port from docker: ${err.message}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function checkBackendHealth(url, maxAttempts = 3, delayMs = 1000) {
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
await axios.get(`${url}/api/v1/health`, {
|
||||
timeout: 5000,
|
||||
validateStatus: (status) => status === 200,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (attempt < maxAttempts) {
|
||||
await new Promise((r) => setTimeout(r, delayMs));
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function discoverBackendUrl() {
|
||||
const dockerUrl = getBackendUrlFromDocker();
|
||||
if (dockerUrl) {
|
||||
log(`Found ${SIGNOZ_INTEGRATION_IMAGE} container, trying ${dockerUrl}...`);
|
||||
if (await checkBackendHealth(dockerUrl)) {
|
||||
log(`Backend found at ${dockerUrl} (from py-test-setup)`);
|
||||
return dockerUrl;
|
||||
}
|
||||
log(`Backend at ${dockerUrl} is not responding`);
|
||||
}
|
||||
|
||||
log(`Trying local backend at ${LOCAL_BACKEND_URL}...`);
|
||||
if (await checkBackendHealth(LOCAL_BACKEND_URL)) {
|
||||
log(`Backend found at ${LOCAL_BACKEND_URL}`);
|
||||
return LOCAL_BACKEND_URL;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchResources(backendUrl) {
|
||||
log('Fetching resources from API...');
|
||||
const resourcesUrl = `${backendUrl}/api/v1/authz/resources`;
|
||||
|
||||
const { data: response } = await axios.get(resourcesUrl);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
function transformResponse(apiResponse) {
|
||||
if (!apiResponse.data) {
|
||||
throw new Error('Invalid API response: missing data field');
|
||||
}
|
||||
|
||||
const { resources, relations } = apiResponse.data;
|
||||
|
||||
return {
|
||||
status: apiResponse.status || 'success',
|
||||
data: {
|
||||
resources: resources,
|
||||
relations: relations,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function generateTypeScriptFile(data) {
|
||||
const resourcesStr = data.data.resources
|
||||
.map(
|
||||
(r) =>
|
||||
`\t\t\t{\n\t\t\t\tname: '${r.name}',\n\t\t\t\ttype: '${r.type}',\n\t\t\t},`,
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
const relationsStr = Object.entries(data.data.relations)
|
||||
.map(
|
||||
([type, relations]) =>
|
||||
`\t\t\t${type}: [${relations.map((r) => `'${r}'`).join(', ')}],`,
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
return `// AUTO GENERATED FILE - DO NOT EDIT - GENERATED BY scripts/generate-permissions-type
|
||||
export default {
|
||||
\tstatus: '${data.status}',
|
||||
\tdata: {
|
||||
\t\tresources: [
|
||||
${resourcesStr}
|
||||
\t\t],
|
||||
\t\trelations: {
|
||||
${relationsStr}
|
||||
\t\t},
|
||||
\t},
|
||||
} as const;
|
||||
`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
log('Starting permissions type generation...');
|
||||
|
||||
const backendUrl = await discoverBackendUrl();
|
||||
|
||||
if (!backendUrl) {
|
||||
console.error('\n' + '='.repeat(80));
|
||||
console.error('ERROR: No running SigNoz backend found!');
|
||||
console.error('='.repeat(80));
|
||||
console.error(
|
||||
'\nThe permissions type generator requires a running SigNoz backend.',
|
||||
);
|
||||
console.error('\nFor local development, start the backend with:');
|
||||
console.error(' make go-run-enterprise');
|
||||
console.error(
|
||||
'\nFor CI or integration testing, start the test environment with:',
|
||||
);
|
||||
console.error(' make py-test-setup');
|
||||
console.error(
|
||||
'\nIf running in CI and seeing this error, check that the py-test-setup',
|
||||
);
|
||||
console.error('step completed successfully before this step runs.');
|
||||
console.error('='.repeat(80) + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log('Fetching resources...');
|
||||
const apiResponse = await fetchResources(backendUrl);
|
||||
|
||||
log('Transforming response...');
|
||||
const transformed = transformResponse(apiResponse);
|
||||
|
||||
log('Generating TypeScript file...');
|
||||
const content = generateTypeScriptFile(transformed);
|
||||
|
||||
log(`Writing to ${PERMISSIONS_TYPE_FILE}...`);
|
||||
fs.writeFileSync(PERMISSIONS_TYPE_FILE, content, 'utf8');
|
||||
|
||||
const rootDir = path.join(__dirname, '../..');
|
||||
const relativePath = path.relative(
|
||||
path.join(rootDir, 'frontend'),
|
||||
PERMISSIONS_TYPE_FILE,
|
||||
);
|
||||
log('Linting generated file...');
|
||||
execSync(`cd frontend && yarn oxlint ${relativePath}`, {
|
||||
cwd: rootDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
log('Successfully generated permissions.type.ts');
|
||||
} catch (error) {
|
||||
log(`Error: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { main };
|
||||
@@ -1,25 +0,0 @@
|
||||
files="";
|
||||
|
||||
# lint-staged will pass all files in $1 $2 $3 etc. iterate and concat.
|
||||
for var in "$@"
|
||||
do
|
||||
files="$files \"$var\","
|
||||
done
|
||||
|
||||
# create temporary tsconfig which includes only passed files
|
||||
str="{
|
||||
\"extends\": \"./tsconfig.json\",
|
||||
\"include\": [ \"src/typings/**/*.ts\",\"src/**/*.d.ts\", \"./babel.config.js\", \"./jest.config.ts\", \"./.eslintrc.js\",\"./__mocks__\",\"./public\",\"./tests\",\"./commitlint.config.ts\",\"./webpack.config.js\",\"./webpack.config.prod.js\",\"./jest.setup.ts\",\"./**/*.d.ts\",$files]
|
||||
}"
|
||||
echo $str > tsconfig.tmp
|
||||
|
||||
# run typecheck using temp config
|
||||
tsc -p ./tsconfig.tmp
|
||||
|
||||
# capture exit code of tsc
|
||||
code=$?
|
||||
|
||||
# delete temp config
|
||||
rm ./tsconfig.tmp
|
||||
|
||||
exit $code
|
||||
@@ -65,6 +65,13 @@ export const TraceDetail = Loadable(
|
||||
),
|
||||
);
|
||||
|
||||
export const TraceDetailV3 = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "TraceDetailV3 Page" */ 'pages/TraceDetailV3Page/index'
|
||||
),
|
||||
);
|
||||
|
||||
export const UsageExplorerPage = Loadable(
|
||||
() => import(/* webpackChunkName: "UsageExplorerPage" */ 'modules/Usage'),
|
||||
);
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
StatusPage,
|
||||
SupportPage,
|
||||
TraceDetail,
|
||||
TraceDetailV3,
|
||||
TraceFilter,
|
||||
TracesExplorer,
|
||||
TracesFunnelDetails,
|
||||
@@ -138,6 +139,9 @@ const routes: AppRoutes[] = [
|
||||
exact: true,
|
||||
key: 'LOGS_SAVE_VIEWS',
|
||||
},
|
||||
// V3 trace details is gated until release: /trace serves V2 (public),
|
||||
// /trace-old serves V3 (URL-only access). Flip the two `component`
|
||||
// values back to release V3.
|
||||
{
|
||||
path: ROUTES.TRACE_DETAIL,
|
||||
exact: true,
|
||||
@@ -145,6 +149,13 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'TRACE_DETAIL',
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACE_DETAIL_OLD,
|
||||
exact: true,
|
||||
component: TraceDetailV3,
|
||||
isPrivate: true,
|
||||
key: 'TRACE_DETAIL_OLD',
|
||||
},
|
||||
{
|
||||
path: ROUTES.SETTINGS,
|
||||
exact: false,
|
||||
|
||||
@@ -19,9 +19,11 @@ import type {
|
||||
|
||||
import type {
|
||||
AuthtypesPostableAuthDomainDTO,
|
||||
AuthtypesUpdateableAuthDomainDTO,
|
||||
CreateAuthDomain200,
|
||||
AuthtypesUpdatableAuthDomainDTO,
|
||||
CreateAuthDomain201,
|
||||
DeleteAuthDomainPathParameters,
|
||||
GetAuthDomain200,
|
||||
GetAuthDomainPathParameters,
|
||||
ListAuthDomains200,
|
||||
RenderErrorResponseDTO,
|
||||
UpdateAuthDomainPathParameters,
|
||||
@@ -124,7 +126,7 @@ export const createAuthDomain = (
|
||||
authtypesPostableAuthDomainDTO: BodyType<AuthtypesPostableAuthDomainDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateAuthDomain200>({
|
||||
return GeneratedAPIInstance<CreateAuthDomain201>({
|
||||
url: `/api/v1/domains`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -277,19 +279,122 @@ export const useDeleteAuthDomain = <
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint returns an auth domain by ID
|
||||
* @summary Get auth domain by ID
|
||||
*/
|
||||
export const getAuthDomain = (
|
||||
{ id }: GetAuthDomainPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetAuthDomain200>({
|
||||
url: `/api/v1/domains/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetAuthDomainQueryKey = ({
|
||||
id,
|
||||
}: GetAuthDomainPathParameters) => {
|
||||
return [`/api/v1/domains/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetAuthDomainQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getAuthDomain>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetAuthDomainPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getAuthDomain>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetAuthDomainQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getAuthDomain>>> = ({
|
||||
signal,
|
||||
}) => getAuthDomain({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getAuthDomain>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetAuthDomainQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getAuthDomain>>
|
||||
>;
|
||||
export type GetAuthDomainQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get auth domain by ID
|
||||
*/
|
||||
|
||||
export function useGetAuthDomain<
|
||||
TData = Awaited<ReturnType<typeof getAuthDomain>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetAuthDomainPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getAuthDomain>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetAuthDomainQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get auth domain by ID
|
||||
*/
|
||||
export const invalidateGetAuthDomain = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetAuthDomainPathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetAuthDomainQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint updates an auth domain
|
||||
* @summary Update auth domain
|
||||
*/
|
||||
export const updateAuthDomain = (
|
||||
{ id }: UpdateAuthDomainPathParameters,
|
||||
authtypesUpdateableAuthDomainDTO: BodyType<AuthtypesUpdateableAuthDomainDTO>,
|
||||
authtypesUpdatableAuthDomainDTO: BodyType<AuthtypesUpdatableAuthDomainDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/domains/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: authtypesUpdateableAuthDomainDTO,
|
||||
data: authtypesUpdatableAuthDomainDTO,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -302,7 +407,7 @@ export const getUpdateAuthDomainMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateAuthDomainPathParameters;
|
||||
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
|
||||
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -311,7 +416,7 @@ export const getUpdateAuthDomainMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateAuthDomainPathParameters;
|
||||
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
|
||||
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
@@ -328,7 +433,7 @@ export const getUpdateAuthDomainMutationOptions = <
|
||||
Awaited<ReturnType<typeof updateAuthDomain>>,
|
||||
{
|
||||
pathParams: UpdateAuthDomainPathParameters;
|
||||
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
|
||||
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
@@ -343,7 +448,7 @@ export type UpdateAuthDomainMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateAuthDomain>>
|
||||
>;
|
||||
export type UpdateAuthDomainMutationBody =
|
||||
BodyType<AuthtypesUpdateableAuthDomainDTO>;
|
||||
BodyType<AuthtypesUpdatableAuthDomainDTO>;
|
||||
export type UpdateAuthDomainMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
@@ -358,7 +463,7 @@ export const useUpdateAuthDomain = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateAuthDomainPathParameters;
|
||||
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
|
||||
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -367,7 +472,7 @@ export const useUpdateAuthDomain = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateAuthDomainPathParameters;
|
||||
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
|
||||
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
|
||||
@@ -4,23 +4,16 @@
|
||||
* * regenerate with 'yarn generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { useMutation } from 'react-query';
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
AuthtypesTransactionDTO,
|
||||
AuthzCheck200,
|
||||
AuthzResources200,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
@@ -110,88 +103,3 @@ export const useAuthzCheck = <
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Gets all the available resources
|
||||
* @summary Get resources
|
||||
*/
|
||||
export const authzResources = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<AuthzResources200>({
|
||||
url: `/api/v1/authz/resources`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getAuthzResourcesQueryKey = () => {
|
||||
return [`/api/v1/authz/resources`] as const;
|
||||
};
|
||||
|
||||
export const getAuthzResourcesQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof authzResources>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof authzResources>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getAuthzResourcesQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof authzResources>>> = ({
|
||||
signal,
|
||||
}) => authzResources(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof authzResources>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type AuthzResourcesQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof authzResources>>
|
||||
>;
|
||||
export type AuthzResourcesQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get resources
|
||||
*/
|
||||
|
||||
export function useAuthzResources<
|
||||
TData = Awaited<ReturnType<typeof authzResources>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof authzResources>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getAuthzResourcesQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get resources
|
||||
*/
|
||||
export const invalidateAuthzResources = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getAuthzResourcesQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
AlertmanagertypesPostableChannelDTO,
|
||||
ConfigReceiverDTO,
|
||||
CreateChannel201,
|
||||
DeleteChannelByIDPathParameters,
|
||||
@@ -122,14 +123,14 @@ export const invalidateListChannels = async (
|
||||
* @summary Create notification channel
|
||||
*/
|
||||
export const createChannel = (
|
||||
configReceiverDTO: BodyType<ConfigReceiverDTO>,
|
||||
alertmanagertypesPostableChannelDTO: BodyType<AlertmanagertypesPostableChannelDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateChannel201>({
|
||||
url: `/api/v1/channels`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: configReceiverDTO,
|
||||
data: alertmanagertypesPostableChannelDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
@@ -141,13 +142,13 @@ export const getCreateChannelMutationOptions = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createChannel>>,
|
||||
TError,
|
||||
{ data: BodyType<ConfigReceiverDTO> },
|
||||
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createChannel>>,
|
||||
TError,
|
||||
{ data: BodyType<ConfigReceiverDTO> },
|
||||
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createChannel'];
|
||||
@@ -161,7 +162,7 @@ export const getCreateChannelMutationOptions = <
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createChannel>>,
|
||||
{ data: BodyType<ConfigReceiverDTO> }
|
||||
{ data: BodyType<AlertmanagertypesPostableChannelDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
@@ -174,7 +175,8 @@ export const getCreateChannelMutationOptions = <
|
||||
export type CreateChannelMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createChannel>>
|
||||
>;
|
||||
export type CreateChannelMutationBody = BodyType<ConfigReceiverDTO>;
|
||||
export type CreateChannelMutationBody =
|
||||
BodyType<AlertmanagertypesPostableChannelDTO>;
|
||||
export type CreateChannelMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
@@ -187,13 +189,13 @@ export const useCreateChannel = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createChannel>>,
|
||||
TError,
|
||||
{ data: BodyType<ConfigReceiverDTO> },
|
||||
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createChannel>>,
|
||||
TError,
|
||||
{ data: BodyType<ConfigReceiverDTO> },
|
||||
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getCreateChannelMutationOptions(options);
|
||||
|
||||
399
frontend/src/api/generated/services/llmpricingrules/index.ts
Normal file
399
frontend/src/api/generated/services/llmpricingrules/index.ts
Normal file
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'yarn 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 {
|
||||
DeleteLLMPricingRulePathParameters,
|
||||
GetLLMPricingRule200,
|
||||
GetLLMPricingRulePathParameters,
|
||||
ListLLMPricingRules200,
|
||||
ListLLMPricingRulesParams,
|
||||
LlmpricingruletypesUpdatableLLMPricingRulesDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* Returns all LLM pricing rules for the authenticated org, with pagination.
|
||||
* @summary List pricing rules
|
||||
*/
|
||||
export const listLLMPricingRules = (
|
||||
params?: ListLLMPricingRulesParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListLLMPricingRules200>({
|
||||
url: `/api/v1/llm_pricing_rules`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListLLMPricingRulesQueryKey = (
|
||||
params?: ListLLMPricingRulesParams,
|
||||
) => {
|
||||
return [`/api/v1/llm_pricing_rules`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getListLLMPricingRulesQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listLLMPricingRules>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params?: ListLLMPricingRulesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listLLMPricingRules>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getListLLMPricingRulesQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof listLLMPricingRules>>
|
||||
> = ({ signal }) => listLLMPricingRules(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listLLMPricingRules>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListLLMPricingRulesQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listLLMPricingRules>>
|
||||
>;
|
||||
export type ListLLMPricingRulesQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List pricing rules
|
||||
*/
|
||||
|
||||
export function useListLLMPricingRules<
|
||||
TData = Awaited<ReturnType<typeof listLLMPricingRules>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params?: ListLLMPricingRulesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listLLMPricingRules>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListLLMPricingRulesQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List pricing rules
|
||||
*/
|
||||
export const invalidateListLLMPricingRules = async (
|
||||
queryClient: QueryClient,
|
||||
params?: ListLLMPricingRulesParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListLLMPricingRulesQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Single write endpoint used by both the user and the Zeus sync job. Per-rule match is by id, then sourceId, then insert. Override rows (is_override=true) are fully preserved when the request does not provide isOverride; only synced_at is stamped.
|
||||
* @summary Create or update pricing rules
|
||||
*/
|
||||
export const createOrUpdateLLMPricingRules = (
|
||||
llmpricingruletypesUpdatableLLMPricingRulesDTO: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/llm_pricing_rules`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: llmpricingruletypesUpdatableLLMPricingRulesDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateOrUpdateLLMPricingRulesMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
|
||||
TError,
|
||||
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
|
||||
TError,
|
||||
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createOrUpdateLLMPricingRules'];
|
||||
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 createOrUpdateLLMPricingRules>>,
|
||||
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createOrUpdateLLMPricingRules(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateOrUpdateLLMPricingRulesMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>
|
||||
>;
|
||||
export type CreateOrUpdateLLMPricingRulesMutationBody =
|
||||
BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO>;
|
||||
export type CreateOrUpdateLLMPricingRulesMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create or update pricing rules
|
||||
*/
|
||||
export const useCreateOrUpdateLLMPricingRules = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
|
||||
TError,
|
||||
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
|
||||
TError,
|
||||
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions =
|
||||
getCreateOrUpdateLLMPricingRulesMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Hard-deletes a pricing rule. If auto-synced, it will be recreated on the next sync cycle.
|
||||
* @summary Delete a pricing rule
|
||||
*/
|
||||
export const deleteLLMPricingRule = ({
|
||||
id,
|
||||
}: DeleteLLMPricingRulePathParameters) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/llm_pricing_rules/${id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteLLMPricingRuleMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLLMPricingRulePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLLMPricingRulePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteLLMPricingRule'];
|
||||
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 deleteLLMPricingRule>>,
|
||||
{ pathParams: DeleteLLMPricingRulePathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteLLMPricingRule(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteLLMPricingRuleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteLLMPricingRule>>
|
||||
>;
|
||||
|
||||
export type DeleteLLMPricingRuleMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete a pricing rule
|
||||
*/
|
||||
export const useDeleteLLMPricingRule = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLLMPricingRulePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLLMPricingRulePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getDeleteLLMPricingRuleMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Returns a single LLM pricing rule by ID.
|
||||
* @summary Get a pricing rule
|
||||
*/
|
||||
export const getLLMPricingRule = (
|
||||
{ id }: GetLLMPricingRulePathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetLLMPricingRule200>({
|
||||
url: `/api/v1/llm_pricing_rules/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetLLMPricingRuleQueryKey = ({
|
||||
id,
|
||||
}: GetLLMPricingRulePathParameters) => {
|
||||
return [`/api/v1/llm_pricing_rules/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetLLMPricingRuleQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getLLMPricingRule>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetLLMPricingRulePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getLLMPricingRule>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetLLMPricingRuleQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getLLMPricingRule>>
|
||||
> = ({ signal }) => getLLMPricingRule({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getLLMPricingRule>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetLLMPricingRuleQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getLLMPricingRule>>
|
||||
>;
|
||||
export type GetLLMPricingRuleQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get a pricing rule
|
||||
*/
|
||||
|
||||
export function useGetLLMPricingRule<
|
||||
TData = Awaited<ReturnType<typeof getLLMPricingRule>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetLLMPricingRulePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getLLMPricingRule>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetLLMPricingRuleQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get a pricing rule
|
||||
*/
|
||||
export const invalidateGetLLMPricingRule = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetLLMPricingRulePathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetLLMPricingRuleQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
@@ -18,9 +18,9 @@ import type {
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
AuthtypesPatchableObjectsDTO,
|
||||
AuthtypesPatchableRoleDTO,
|
||||
AuthtypesPostableRoleDTO,
|
||||
CoretypesPatchableObjectsDTO,
|
||||
CreateRole201,
|
||||
DeleteRolePathParameters,
|
||||
GetObjects200,
|
||||
@@ -571,13 +571,13 @@ export const invalidateGetObjects = async (
|
||||
*/
|
||||
export const patchObjects = (
|
||||
{ id, relation }: PatchObjectsPathParameters,
|
||||
authtypesPatchableObjectsDTO: BodyType<AuthtypesPatchableObjectsDTO>,
|
||||
coretypesPatchableObjectsDTO: BodyType<CoretypesPatchableObjectsDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v1/roles/${id}/relations/${relation}/objects`,
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: authtypesPatchableObjectsDTO,
|
||||
data: coretypesPatchableObjectsDTO,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -590,7 +590,7 @@ export const getPatchObjectsMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchObjectsPathParameters;
|
||||
data: BodyType<AuthtypesPatchableObjectsDTO>;
|
||||
data: BodyType<CoretypesPatchableObjectsDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -599,7 +599,7 @@ export const getPatchObjectsMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchObjectsPathParameters;
|
||||
data: BodyType<AuthtypesPatchableObjectsDTO>;
|
||||
data: BodyType<CoretypesPatchableObjectsDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
@@ -616,7 +616,7 @@ export const getPatchObjectsMutationOptions = <
|
||||
Awaited<ReturnType<typeof patchObjects>>,
|
||||
{
|
||||
pathParams: PatchObjectsPathParameters;
|
||||
data: BodyType<AuthtypesPatchableObjectsDTO>;
|
||||
data: BodyType<CoretypesPatchableObjectsDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
@@ -630,7 +630,7 @@ export const getPatchObjectsMutationOptions = <
|
||||
export type PatchObjectsMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof patchObjects>>
|
||||
>;
|
||||
export type PatchObjectsMutationBody = BodyType<AuthtypesPatchableObjectsDTO>;
|
||||
export type PatchObjectsMutationBody = BodyType<CoretypesPatchableObjectsDTO>;
|
||||
export type PatchObjectsMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
@@ -645,7 +645,7 @@ export const usePatchObjects = <
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchObjectsPathParameters;
|
||||
data: BodyType<AuthtypesPatchableObjectsDTO>;
|
||||
data: BodyType<CoretypesPatchableObjectsDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -654,7 +654,7 @@ export const usePatchObjects = <
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchObjectsPathParameters;
|
||||
data: BodyType<AuthtypesPatchableObjectsDTO>;
|
||||
data: BodyType<CoretypesPatchableObjectsDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
72
frontend/src/api/trace/getTraceV3.tsx
Normal file
72
frontend/src/api/trace/getTraceV3.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { ApiV3Instance as axios } from 'api';
|
||||
import { omit } from 'lodash-es';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
GetTraceV3PayloadProps,
|
||||
GetTraceV3SuccessResponse,
|
||||
SpanV3,
|
||||
} from 'types/api/trace/getTraceV3';
|
||||
|
||||
const getTraceV3 = async (
|
||||
props: GetTraceV3PayloadProps,
|
||||
): Promise<SuccessResponse<GetTraceV3SuccessResponse> | ErrorResponse> => {
|
||||
let uncollapsedSpans = [...props.uncollapsedSpans];
|
||||
if (!props.isSelectedSpanIDUnCollapsed) {
|
||||
uncollapsedSpans = uncollapsedSpans.filter(
|
||||
(node) => node !== props.selectedSpanId,
|
||||
);
|
||||
} else if (
|
||||
props.selectedSpanId &&
|
||||
!uncollapsedSpans.includes(props.selectedSpanId)
|
||||
) {
|
||||
// V3 backend only uses uncollapsedSpans list (unlike V2 which also interprets
|
||||
// isSelectedSpanIDUnCollapsed server-side), so explicitly add the selected span
|
||||
uncollapsedSpans.push(props.selectedSpanId);
|
||||
}
|
||||
const postData: GetTraceV3PayloadProps = {
|
||||
...props,
|
||||
uncollapsedSpans,
|
||||
limit: 10000,
|
||||
};
|
||||
const response = await axios.post<GetTraceV3SuccessResponse>(
|
||||
`/traces/${props.traceId}/waterfall`,
|
||||
omit(postData, 'traceId'),
|
||||
);
|
||||
|
||||
// V3 API wraps response in { status, data }
|
||||
const rawPayload = (response.data as any).data || response.data;
|
||||
|
||||
// Derive 'service.name' from resource for convenience — only derived field
|
||||
const spans: SpanV3[] = (rawPayload.spans || []).map((span: any) => ({
|
||||
...span,
|
||||
'service.name': span.resource?.['service.name'] || '',
|
||||
}));
|
||||
|
||||
// V3 API returns startTimestampMillis/endTimestampMillis as relative durations (ms from epoch offset),
|
||||
// not absolute unix millis like V2. The span timestamps are absolute unix millis.
|
||||
// Convert by using the first span's timestamp as the base if there's a mismatch.
|
||||
let { startTimestampMillis, endTimestampMillis } = rawPayload;
|
||||
if (
|
||||
spans.length > 0 &&
|
||||
spans[0].timestamp > 0 &&
|
||||
startTimestampMillis < spans[0].timestamp / 10
|
||||
) {
|
||||
const durationMillis = endTimestampMillis - startTimestampMillis;
|
||||
startTimestampMillis = spans[0].timestamp;
|
||||
endTimestampMillis = startTimestampMillis + durationMillis;
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: {
|
||||
...rawPayload,
|
||||
spans,
|
||||
startTimestampMillis,
|
||||
endTimestampMillis,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default getTraceV3;
|
||||
@@ -1,33 +0,0 @@
|
||||
.error-state-container {
|
||||
height: 240px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
border-radius: 3px;
|
||||
|
||||
.error-state-container-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.error-state-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-state-additional-messages {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.error-state-additional-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Typography } from 'antd';
|
||||
|
||||
import APIError from '../../types/api/error';
|
||||
|
||||
import './Common.styles.scss';
|
||||
|
||||
interface ErrorStateComponentProps {
|
||||
message?: string;
|
||||
error?: APIError;
|
||||
}
|
||||
|
||||
const defaultProps: Partial<ErrorStateComponentProps> = {
|
||||
message: undefined,
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
function ErrorStateComponent({
|
||||
message,
|
||||
error,
|
||||
}: ErrorStateComponentProps): JSX.Element {
|
||||
// Handle API Error object
|
||||
if (error) {
|
||||
const mainMessage = error.getErrorMessage();
|
||||
const additionalErrors = error.getErrorDetails().error.errors || [];
|
||||
|
||||
return (
|
||||
<div className="error-state-container">
|
||||
<div className="error-state-container-content">
|
||||
<Typography className="error-state-text">{mainMessage}</Typography>
|
||||
{additionalErrors.length > 0 && (
|
||||
<div className="error-state-additional-messages">
|
||||
{additionalErrors.map((additionalError) => (
|
||||
<Typography
|
||||
key={`error-${additionalError.message}`}
|
||||
className="error-state-additional-text"
|
||||
>
|
||||
• {additionalError.message}
|
||||
</Typography>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle simple string message (backwards compatibility)
|
||||
return (
|
||||
<div className="error-state-container">
|
||||
<div className="error-state-container-content">
|
||||
<Typography className="error-state-text">{message}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ErrorStateComponent.defaultProps = defaultProps;
|
||||
|
||||
export default ErrorStateComponent;
|
||||
@@ -1,4 +0,0 @@
|
||||
.custom-date-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { DatePicker } from 'antd';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||
import {
|
||||
CustomTimeType,
|
||||
LexicalContext,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import './RangePickerModal.styles.scss';
|
||||
|
||||
interface RangePickerModalProps {
|
||||
setCustomDTPickerVisible: Dispatch<SetStateAction<boolean>>;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
onCustomDateHandler: (
|
||||
dateTimeRange: DateTimeRangeType,
|
||||
lexicalContext?: LexicalContext | undefined,
|
||||
) => void;
|
||||
selectedTime: string;
|
||||
onTimeChange?: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
}
|
||||
|
||||
function RangePickerModal(props: RangePickerModalProps): JSX.Element {
|
||||
const {
|
||||
setCustomDTPickerVisible,
|
||||
setIsOpen,
|
||||
onCustomDateHandler,
|
||||
selectedTime,
|
||||
onTimeChange,
|
||||
} = props;
|
||||
const { RangePicker } = DatePicker;
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
// Using any type here because antd's DatePicker expects its own internal Dayjs type
|
||||
// which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc).
|
||||
const disabledDate = (current: any): boolean => {
|
||||
const currentDay = dayjs(current);
|
||||
return currentDay.isAfter(dayjs());
|
||||
};
|
||||
|
||||
const onPopoverClose = (visible: boolean): void => {
|
||||
if (!visible) {
|
||||
setCustomDTPickerVisible(false);
|
||||
}
|
||||
setIsOpen(visible);
|
||||
};
|
||||
|
||||
const onModalOkHandler = (date_time: any): void => {
|
||||
if (date_time?.[1]) {
|
||||
onPopoverClose(false);
|
||||
}
|
||||
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
|
||||
};
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
const rangeValue: [Dayjs, Dayjs] = useMemo(
|
||||
() => [
|
||||
dayjs(minTime / 1000_000).tz(timezone.value),
|
||||
dayjs(maxTime / 1000_000).tz(timezone.value),
|
||||
],
|
||||
[maxTime, minTime, timezone.value],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="custom-date-picker">
|
||||
<RangePicker
|
||||
disabledDate={disabledDate}
|
||||
allowClear
|
||||
showTime
|
||||
format={(date: Dayjs): string =>
|
||||
date.tz(timezone.value).format(DATE_TIME_FORMATS.ISO_DATETIME)
|
||||
}
|
||||
onOk={onModalOkHandler}
|
||||
data-1p-ignore
|
||||
{...(selectedTime === 'custom' &&
|
||||
!onTimeChange && {
|
||||
value: rangeValue,
|
||||
})}
|
||||
// use default value if onTimeChange is provided
|
||||
{...(selectedTime === 'custom' &&
|
||||
onTimeChange && {
|
||||
defaultValue: rangeValue,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RangePickerModal.defaultProps = {
|
||||
onTimeChange: undefined,
|
||||
};
|
||||
|
||||
export default RangePickerModal;
|
||||
@@ -1,93 +0,0 @@
|
||||
.details-drawer {
|
||||
.ant-drawer-wrapper-body {
|
||||
border-left: 1px solid var(--l1-border);
|
||||
}
|
||||
.ant-drawer-header {
|
||||
background: var(--l2-background);
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.ant-drawer-header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ant-drawer-close {
|
||||
margin-inline-end: 0px;
|
||||
padding: 0px;
|
||||
padding-right: 16px;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.ant-drawer-title {
|
||||
padding-left: 16px;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-drawer-body {
|
||||
padding: 16px;
|
||||
background: var(--l2-background);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.details-drawer-tabs {
|
||||
margin-top: 32px;
|
||||
|
||||
.ant-tabs-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 114px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
padding: 7px 20px;
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: -0.06px;
|
||||
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.ant-btn:hover {
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab-active {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
.ant-tabs-tab + .ant-tabs-tab {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.ant-tabs-nav::before {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.ant-tabs-ink-bar {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Drawer, Tabs, TabsProps } from 'antd';
|
||||
import cx from 'classnames';
|
||||
|
||||
import './DetailsDrawer.styles.scss';
|
||||
|
||||
interface IDetailsDrawerProps {
|
||||
open: boolean;
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
title: string;
|
||||
descriptiveContent: JSX.Element;
|
||||
defaultActiveKey: string;
|
||||
items: TabsProps['items'];
|
||||
detailsDrawerClassName?: string;
|
||||
tabBarExtraContent?: JSX.Element;
|
||||
}
|
||||
|
||||
function DetailsDrawer(props: IDetailsDrawerProps): JSX.Element {
|
||||
const {
|
||||
open,
|
||||
setOpen,
|
||||
title,
|
||||
descriptiveContent,
|
||||
defaultActiveKey,
|
||||
detailsDrawerClassName,
|
||||
items,
|
||||
tabBarExtraContent,
|
||||
} = props;
|
||||
return (
|
||||
<Drawer
|
||||
width="60%"
|
||||
open={open}
|
||||
afterOpenChange={setOpen}
|
||||
mask={false}
|
||||
title={title}
|
||||
onClose={(): void => setOpen(false)}
|
||||
className="details-drawer"
|
||||
>
|
||||
<div>{descriptiveContent}</div>
|
||||
<Tabs
|
||||
items={items}
|
||||
addIcon
|
||||
defaultActiveKey={defaultActiveKey}
|
||||
animated
|
||||
className={cx('details-drawer-tabs', detailsDrawerClassName)}
|
||||
tabBarExtraContent={tabBarExtraContent}
|
||||
/>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
DetailsDrawer.defaultProps = {
|
||||
detailsDrawerClassName: '',
|
||||
tabBarExtraContent: null,
|
||||
};
|
||||
|
||||
export default DetailsDrawer;
|
||||
@@ -0,0 +1,37 @@
|
||||
.details-header {
|
||||
// ghost + secondary missing hover bg token in @signozhq/button
|
||||
--button-ghost-hover-background: var(--l3-background);
|
||||
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
height: 36px;
|
||||
background: var(--l2-background);
|
||||
|
||||
&__title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--l1-foreground);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Button } from '@signozhq/ui';
|
||||
import { X } from '@signozhq/icons';
|
||||
|
||||
import './DetailsHeader.styles.scss';
|
||||
|
||||
export interface HeaderAction {
|
||||
key: string;
|
||||
component: ReactNode; // check later if we can use direct btn itself or not.
|
||||
}
|
||||
|
||||
export interface DetailsHeaderProps {
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
actions?: HeaderAction[];
|
||||
closePosition?: 'left' | 'right';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function DetailsHeader({
|
||||
title,
|
||||
onClose,
|
||||
actions,
|
||||
closePosition = 'right',
|
||||
className,
|
||||
}: DetailsHeaderProps): JSX.Element {
|
||||
const closeButton = (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
prefix={<X size={14} />}
|
||||
></Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`details-header ${className || ''}`}>
|
||||
{closePosition === 'left' && closeButton}
|
||||
|
||||
<span className="details-header__title">{title}</span>
|
||||
|
||||
{actions && (
|
||||
<div className="details-header__actions">
|
||||
{actions.map((action) => (
|
||||
<div key={action.key}>{action.component}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{closePosition === 'right' && closeButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailsHeader;
|
||||
@@ -0,0 +1,7 @@
|
||||
.details-panel-drawer {
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
35
frontend/src/components/DetailsPanel/DetailsPanelDrawer.tsx
Normal file
35
frontend/src/components/DetailsPanel/DetailsPanelDrawer.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { DrawerWrapper } from '@signozhq/ui';
|
||||
|
||||
import './DetailsPanelDrawer.styles.scss';
|
||||
|
||||
interface DetailsPanelDrawerProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function DetailsPanelDrawer({
|
||||
isOpen,
|
||||
onClose,
|
||||
children,
|
||||
className,
|
||||
}: DetailsPanelDrawerProps): JSX.Element {
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={isOpen}
|
||||
onOpenChange={(open): void => {
|
||||
if (!open) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
direction="right"
|
||||
showOverlay={false}
|
||||
className={`details-panel-drawer ${className || ''}`}
|
||||
>
|
||||
<div className="details-panel-drawer__body">{children}</div>
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailsPanelDrawer;
|
||||
8
frontend/src/components/DetailsPanel/index.ts
Normal file
8
frontend/src/components/DetailsPanel/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type {
|
||||
DetailsHeaderProps,
|
||||
HeaderAction,
|
||||
} from './DetailsHeader/DetailsHeader';
|
||||
export { default as DetailsHeader } from './DetailsHeader/DetailsHeader';
|
||||
export { default as DetailsPanelDrawer } from './DetailsPanelDrawer';
|
||||
export type { DetailsPanelState, UseDetailsPanelOptions } from './types';
|
||||
export { default as useDetailsPanel } from './useDetailsPanel';
|
||||
10
frontend/src/components/DetailsPanel/types.ts
Normal file
10
frontend/src/components/DetailsPanel/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface DetailsPanelState {
|
||||
isOpen: boolean;
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export interface UseDetailsPanelOptions {
|
||||
entityId: string | undefined;
|
||||
onClose?: () => void;
|
||||
}
|
||||
29
frontend/src/components/DetailsPanel/useDetailsPanel.ts
Normal file
29
frontend/src/components/DetailsPanel/useDetailsPanel.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { DetailsPanelState, UseDetailsPanelOptions } from './types';
|
||||
|
||||
function useDetailsPanel({
|
||||
entityId,
|
||||
onClose,
|
||||
}: UseDetailsPanelOptions): DetailsPanelState {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const prevEntityIdRef = useRef<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const currentId = entityId || '';
|
||||
if (currentId && currentId !== prevEntityIdRef.current) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
prevEntityIdRef.current = currentId;
|
||||
}, [entityId]);
|
||||
|
||||
const open = useCallback(() => setIsOpen(true), []);
|
||||
const close = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
onClose?.();
|
||||
}, [onClose]);
|
||||
|
||||
return { isOpen, open, close };
|
||||
}
|
||||
|
||||
export default useDetailsPanel;
|
||||
@@ -46,6 +46,7 @@ function DeleteMemberDialog({
|
||||
color="destructive"
|
||||
disabled={isDeleting}
|
||||
onClick={onConfirm}
|
||||
loading={isDeleting}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{isDeleting ? 'Processing...' : title}
|
||||
@@ -63,7 +64,6 @@ function DeleteMemberDialog({
|
||||
}}
|
||||
title={title}
|
||||
width="narrow"
|
||||
className="alert-dialog delete-dialog"
|
||||
showCloseButton={false}
|
||||
disableOutsideClick={false}
|
||||
footer={footer}
|
||||
|
||||
@@ -28,18 +28,6 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&__input {
|
||||
height: 32px;
|
||||
background: var(--l2-background);
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
box-shadow: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -48,7 +36,7 @@
|
||||
padding: var(--padding-1) var(--padding-2);
|
||||
border-radius: 2px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
border: 1px solid var(--border);
|
||||
box-sizing: border-box;
|
||||
|
||||
&--disabled {
|
||||
@@ -65,8 +53,8 @@
|
||||
}
|
||||
|
||||
&__email-text {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
font-weight: var(--paragraph-base-400-font-weight);
|
||||
color: var(--foreground);
|
||||
line-height: var(--line-height-18);
|
||||
letter-spacing: -0.07px;
|
||||
@@ -178,36 +166,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.delete-dialog {
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
|
||||
[data-slot='dialog-title'] {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__body {
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
font-weight: var(--paragraph-base-400-font-weight);
|
||||
color: var(--l2-foreground);
|
||||
line-height: var(--paragraph-base-400-line-height);
|
||||
letter-spacing: -0.065px;
|
||||
margin: 0;
|
||||
|
||||
strong {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-4);
|
||||
margin-top: var(--margin-6);
|
||||
}
|
||||
}
|
||||
|
||||
.reset-link-dialog {
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
@@ -264,13 +222,6 @@
|
||||
}
|
||||
|
||||
&__copy-btn {
|
||||
flex-shrink: 0;
|
||||
height: 32px;
|
||||
border-radius: 0 2px 2px 0;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
border-left: 1px solid var(--l1-border);
|
||||
min-width: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ function EditMemberDrawer({
|
||||
try {
|
||||
await rawRetry();
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== context));
|
||||
refetchUser();
|
||||
void refetchUser();
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
@@ -250,7 +250,7 @@ function EditMemberDrawer({
|
||||
});
|
||||
}
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Name update'));
|
||||
refetchUser();
|
||||
void refetchUser();
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
@@ -319,7 +319,7 @@ function EditMemberDrawer({
|
||||
}),
|
||||
];
|
||||
});
|
||||
refetchUser();
|
||||
void refetchUser();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -340,7 +340,7 @@ function EditMemberDrawer({
|
||||
onComplete();
|
||||
}
|
||||
|
||||
refetchUser();
|
||||
void refetchUser();
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -465,7 +465,6 @@ function EditMemberDrawer({
|
||||
prev.filter((err) => err.context !== 'Name update'),
|
||||
);
|
||||
}}
|
||||
className="edit-member-drawer__input"
|
||||
placeholder="Enter name"
|
||||
disabled={isRootUser || isDeleted}
|
||||
/>
|
||||
@@ -631,7 +630,7 @@ function EditMemberDrawer({
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__footer-right">
|
||||
<Button variant="solid" color="secondary" onClick={handleClose}>
|
||||
<Button variant="outlined" color="secondary" onClick={handleClose}>
|
||||
<X size={14} />
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -641,6 +640,7 @@ function EditMemberDrawer({
|
||||
color="primary"
|
||||
disabled={!isDirty || isSaving || isRootUser}
|
||||
onClick={handleSave}
|
||||
loading={isSaving}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Member Details'}
|
||||
</Button>
|
||||
|
||||
@@ -44,9 +44,8 @@ function ResetLinkDialog({
|
||||
<span className="reset-link-dialog__link-text">{resetLink}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
variant="link"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={onCopy}
|
||||
prefix={hasCopied ? <Check size={12} /> : <Copy size={12} />}
|
||||
className="reset-link-dialog__copy-btn"
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from 'antd';
|
||||
|
||||
import { withErrorBoundary } from './index';
|
||||
|
||||
/**
|
||||
* Example component that can throw errors
|
||||
*/
|
||||
function ProblematicComponent(): JSX.Element {
|
||||
const [shouldThrow, setShouldThrow] = useState(false);
|
||||
|
||||
if (shouldThrow) {
|
||||
throw new Error('This is a test error from ProblematicComponent!');
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h3>Problematic Component</h3>
|
||||
<p>This component can throw errors when the button is clicked.</p>
|
||||
<Button type="primary" onClick={(): void => setShouldThrow(true)} danger>
|
||||
Trigger Error
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic usage - wraps component with default error boundary
|
||||
*/
|
||||
export const SafeProblematicComponent = withErrorBoundary(ProblematicComponent);
|
||||
|
||||
/**
|
||||
* Usage with custom fallback component
|
||||
*/
|
||||
function CustomErrorFallback(): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
style={{ padding: '20px', border: '1px solid red', borderRadius: '4px' }}
|
||||
>
|
||||
<h4 style={{ color: 'red' }}>Custom Error Fallback</h4>
|
||||
<p>Something went wrong in this specific component!</p>
|
||||
<Button onClick={(): void => window.location.reload()}>Reload Page</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const SafeProblematicComponentWithCustomFallback = withErrorBoundary(
|
||||
ProblematicComponent,
|
||||
{
|
||||
fallback: <CustomErrorFallback />,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Usage with custom error handler
|
||||
*/
|
||||
export const SafeProblematicComponentWithErrorHandler = withErrorBoundary(
|
||||
ProblematicComponent,
|
||||
{
|
||||
onError: (error, errorInfo) => {
|
||||
console.error('Custom error handler:', error);
|
||||
console.error('Error info:', errorInfo);
|
||||
// You could also send to analytics, logging service, etc.
|
||||
},
|
||||
sentryOptions: {
|
||||
tags: {
|
||||
section: 'dashboard',
|
||||
priority: 'high',
|
||||
},
|
||||
level: 'error',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Example of wrapping an existing component from the codebase
|
||||
*/
|
||||
function ExistingComponent({
|
||||
title,
|
||||
data,
|
||||
}: {
|
||||
title: string;
|
||||
data: any[];
|
||||
}): JSX.Element {
|
||||
// This could be any existing component that might throw errors
|
||||
return (
|
||||
<div>
|
||||
<h4>{title}</h4>
|
||||
<ul>
|
||||
{data.map((item, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<li key={index}>{item.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const SafeExistingComponent = withErrorBoundary(ExistingComponent, {
|
||||
sentryOptions: {
|
||||
tags: {
|
||||
component: 'ExistingComponent',
|
||||
feature: 'data-display',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Usage examples in a container component
|
||||
*/
|
||||
export function ErrorBoundaryExamples(): JSX.Element {
|
||||
const sampleData = [
|
||||
{ name: 'Item 1' },
|
||||
{ name: 'Item 2' },
|
||||
{ name: 'Item 3' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h2>Error Boundary HOC Examples</h2>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h3>1. Basic Usage</h3>
|
||||
<SafeProblematicComponent />
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h3>2. With Custom Fallback</h3>
|
||||
<SafeProblematicComponentWithCustomFallback />
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h3>3. With Custom Error Handler</h3>
|
||||
<SafeProblematicComponentWithErrorHandler />
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h3>4. Wrapped Existing Component</h3>
|
||||
<SafeExistingComponent title="Sample Data" data={sampleData} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -55,7 +55,7 @@ describe('GuardAuthZ', () => {
|
||||
);
|
||||
|
||||
render(
|
||||
<GuardAuthZ relation="read" object="dashboard:*">
|
||||
<GuardAuthZ relation="read" object="role:*">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
@@ -79,7 +79,7 @@ describe('GuardAuthZ', () => {
|
||||
render(
|
||||
<GuardAuthZ
|
||||
relation="read"
|
||||
object="dashboard:*"
|
||||
object="role:*"
|
||||
fallbackOnLoading={<LoadingFallback />}
|
||||
>
|
||||
<TestChild />
|
||||
@@ -102,7 +102,7 @@ describe('GuardAuthZ', () => {
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
<GuardAuthZ relation="read" object="dashboard:*">
|
||||
<GuardAuthZ relation="read" object="role:*">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
@@ -121,11 +121,7 @@ describe('GuardAuthZ', () => {
|
||||
);
|
||||
|
||||
render(
|
||||
<GuardAuthZ
|
||||
relation="read"
|
||||
object="dashboard:*"
|
||||
fallbackOnError={ErrorFallback}
|
||||
>
|
||||
<GuardAuthZ relation="read" object="role:*" fallbackOnError={ErrorFallback}>
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
@@ -155,7 +151,7 @@ describe('GuardAuthZ', () => {
|
||||
render(
|
||||
<GuardAuthZ
|
||||
relation="read"
|
||||
object="dashboard:*"
|
||||
object="role:*"
|
||||
fallbackOnError={errorFallbackWithCapture}
|
||||
>
|
||||
<TestChild />
|
||||
@@ -178,7 +174,7 @@ describe('GuardAuthZ', () => {
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
<GuardAuthZ relation="read" object="dashboard:*">
|
||||
<GuardAuthZ relation="read" object="role:*">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
@@ -201,7 +197,7 @@ describe('GuardAuthZ', () => {
|
||||
render(
|
||||
<GuardAuthZ
|
||||
relation="update"
|
||||
object="dashboard:123"
|
||||
object="role:123"
|
||||
fallbackOnNoPermissions={NoPermissionFallback}
|
||||
>
|
||||
<TestChild />
|
||||
@@ -224,7 +220,7 @@ describe('GuardAuthZ', () => {
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
<GuardAuthZ relation="update" object="dashboard:123">
|
||||
<GuardAuthZ relation="update" object="role:123">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
@@ -244,7 +240,7 @@ describe('GuardAuthZ', () => {
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
<GuardAuthZ relation="read" object="dashboard:*">
|
||||
<GuardAuthZ relation="read" object="role:*">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
@@ -257,7 +253,7 @@ describe('GuardAuthZ', () => {
|
||||
});
|
||||
|
||||
it('should pass requiredPermissionName to fallbackOnNoPermissions', async () => {
|
||||
const permission = buildPermission('update', 'dashboard:123');
|
||||
const permission = buildPermission('update', 'role:123');
|
||||
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
@@ -269,7 +265,7 @@ describe('GuardAuthZ', () => {
|
||||
render(
|
||||
<GuardAuthZ
|
||||
relation="update"
|
||||
object="dashboard:123"
|
||||
object="role:123"
|
||||
fallbackOnNoPermissions={NoPermissionFallbackWithSuggestions}
|
||||
>
|
||||
<TestChild />
|
||||
@@ -299,7 +295,7 @@ describe('GuardAuthZ', () => {
|
||||
);
|
||||
|
||||
const { rerender } = render(
|
||||
<GuardAuthZ relation="read" object="dashboard:*">
|
||||
<GuardAuthZ relation="read" object="role:*">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
@@ -309,7 +305,7 @@ describe('GuardAuthZ', () => {
|
||||
});
|
||||
|
||||
rerender(
|
||||
<GuardAuthZ relation="delete" object="dashboard:456">
|
||||
<GuardAuthZ relation="delete" object="role:456">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Style } from '@signozhq/design-tokens';
|
||||
import { ChevronDown, CircleAlert, Plus, Trash2, X } from '@signozhq/icons';
|
||||
import { ChevronDown, Plus, Trash2, X } from '@signozhq/icons';
|
||||
import {
|
||||
Button,
|
||||
Callout,
|
||||
@@ -294,10 +294,8 @@ function InviteMembersModal({
|
||||
type="error"
|
||||
size="small"
|
||||
showIcon
|
||||
icon={<CircleAlert size={12} />}
|
||||
>
|
||||
{getValidationErrorMessage()}
|
||||
</Callout>
|
||||
title={getValidationErrorMessage()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
.query-builder-search-wrapper {
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-bottom: none;
|
||||
|
||||
.ant-select-selector {
|
||||
border: none !important;
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { Dispatch, SetStateAction, useEffect } from 'react';
|
||||
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import './QueryBuilderSearchWrapper.styles.scss';
|
||||
|
||||
function QueryBuilderSearchWrapper({
|
||||
log,
|
||||
filters,
|
||||
contextQuery,
|
||||
isEdit,
|
||||
suffixIcon,
|
||||
setFilters,
|
||||
setContextQuery,
|
||||
}: QueryBuilderSearchWraperProps): JSX.Element {
|
||||
const initialContextQuery = useInitialQuery(log);
|
||||
|
||||
useEffect(() => {
|
||||
setContextQuery(initialContextQuery);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleSearch = (tagFilters: TagFilter): void => {
|
||||
const tagFiltersLength = tagFilters.items.length;
|
||||
|
||||
if (
|
||||
(!tagFiltersLength && (!filters || !filters.items.length)) ||
|
||||
tagFiltersLength === filters?.items.length ||
|
||||
!contextQuery
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextQuery: Query = {
|
||||
...contextQuery,
|
||||
builder: {
|
||||
...contextQuery.builder,
|
||||
queryData: contextQuery.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
filters: tagFilters,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
setFilters({ ...tagFilters });
|
||||
setContextQuery({ ...nextQuery });
|
||||
};
|
||||
|
||||
if (!contextQuery || !isEdit) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryBuilderSearch
|
||||
query={contextQuery?.builder.queryData[0]}
|
||||
onChange={handleSearch}
|
||||
className="query-builder-search-wrapper"
|
||||
suffixIcon={suffixIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface QueryBuilderSearchWraperProps {
|
||||
log: ILog;
|
||||
isEdit: boolean;
|
||||
contextQuery: Query | undefined;
|
||||
setContextQuery: Dispatch<SetStateAction<Query | undefined>>;
|
||||
filters: TagFilter | null;
|
||||
setFilters: Dispatch<SetStateAction<TagFilter | null>>;
|
||||
suffixIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
QueryBuilderSearchWrapper.defaultProps = {
|
||||
suffixIcon: undefined,
|
||||
};
|
||||
|
||||
export default QueryBuilderSearchWrapper;
|
||||
@@ -17,7 +17,6 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
|
||||
import InfraMetrics from 'container/LogDetailedView/InfraMetrics/InfraMetrics';
|
||||
import JSONView from 'container/LogDetailedView/JsonView';
|
||||
import Overview from 'container/LogDetailedView/Overview';
|
||||
import {
|
||||
aggregateAttributesResourcesToString,
|
||||
@@ -47,6 +46,7 @@ import {
|
||||
TextSelect,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { JsonView } from 'periscope/components/JsonView';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -562,7 +562,9 @@ function LogDetailInner({
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
|
||||
{selectedView === VIEW_TYPES.JSON && (
|
||||
<JsonView data={LogJsonData} height="68vh" />
|
||||
)}
|
||||
|
||||
{selectedView === VIEW_TYPES.CONTEXT && (
|
||||
<ContextView
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
export const rawLineStyle: CSSProperties = {};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Button } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ButtonContainer = styled(Button)`
|
||||
&&& {
|
||||
padding-left: 0;
|
||||
}
|
||||
`;
|
||||
@@ -1,13 +0,0 @@
|
||||
.custom-multiselect-dropdown {
|
||||
.divider {
|
||||
height: 1px;
|
||||
background-color: #e8e8e8;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.all-option {
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
.loading-panel-data {
|
||||
padding: 24px 0;
|
||||
height: 240px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
|
||||
.loading-panel-data-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
.loading-gif {
|
||||
height: 72px;
|
||||
margin-left: -24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Typography } from 'antd';
|
||||
|
||||
import loadingPlaneUrl from '@/assets/Icons/loading-plane.gif';
|
||||
|
||||
import './PanelDataLoading.styles.scss';
|
||||
|
||||
export function PanelDataLoading(): JSX.Element {
|
||||
return (
|
||||
<div className="loading-panel-data">
|
||||
<div className="loading-panel-data-content">
|
||||
<img className="loading-gif" src={loadingPlaneUrl} alt="wait-icon" />
|
||||
|
||||
<Typography.Text>Fetching data...</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
getUserExpressionFromCombined,
|
||||
} from '../utils';
|
||||
import { QuerySearchV2Context } from './context';
|
||||
import type { QuerySearchV2ContextValue } from './QuerySearchV2.store';
|
||||
import { createExpressionStore } from './QuerySearchV2.store';
|
||||
|
||||
export interface QuerySearchV2ProviderProps {
|
||||
queryParamKey: string;
|
||||
initialExpression?: string;
|
||||
/**
|
||||
* @default false
|
||||
*/
|
||||
persistOnUnmount?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider component that creates a scoped zustand store and exposes
|
||||
* expression state to children via context.
|
||||
*/
|
||||
export function QuerySearchV2Provider({
|
||||
initialExpression = '',
|
||||
persistOnUnmount = false,
|
||||
queryParamKey,
|
||||
children,
|
||||
}: QuerySearchV2ProviderProps): JSX.Element {
|
||||
const storeRef = useRef(createExpressionStore());
|
||||
const store = storeRef.current;
|
||||
|
||||
const [urlExpression, setUrlExpression] = useQueryState(
|
||||
queryParamKey,
|
||||
parseAsString,
|
||||
);
|
||||
|
||||
const committedExpression = useStore(store, (s) => s.committedExpression);
|
||||
const setInputExpression = useStore(store, (s) => s.setInputExpression);
|
||||
const commitExpression = useStore(store, (s) => s.commitExpression);
|
||||
const initializeFromUrl = useStore(store, (s) => s.initializeFromUrl);
|
||||
const resetExpression = useStore(store, (s) => s.resetExpression);
|
||||
|
||||
const isInitialized = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!isInitialized.current && urlExpression) {
|
||||
const cleanedExpression = getUserExpressionFromCombined(
|
||||
initialExpression,
|
||||
urlExpression,
|
||||
);
|
||||
initializeFromUrl(cleanedExpression);
|
||||
isInitialized.current = true;
|
||||
}
|
||||
}, [urlExpression, initialExpression, initializeFromUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized.current || !urlExpression) {
|
||||
setUrlExpression(committedExpression || null);
|
||||
}
|
||||
}, [committedExpression, setUrlExpression, urlExpression]);
|
||||
|
||||
useEffect(() => {
|
||||
return (): void => {
|
||||
if (!persistOnUnmount) {
|
||||
setUrlExpression(null);
|
||||
resetExpression();
|
||||
}
|
||||
};
|
||||
}, [persistOnUnmount, setUrlExpression, resetExpression]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(expression: string): void => {
|
||||
const userOnly = getUserExpressionFromCombined(
|
||||
initialExpression,
|
||||
expression,
|
||||
);
|
||||
setInputExpression(userOnly);
|
||||
},
|
||||
[initialExpression, setInputExpression],
|
||||
);
|
||||
|
||||
const handleRun = useCallback(
|
||||
(expression: string): void => {
|
||||
const userOnly = getUserExpressionFromCombined(
|
||||
initialExpression,
|
||||
expression,
|
||||
);
|
||||
commitExpression(userOnly);
|
||||
},
|
||||
[initialExpression, commitExpression],
|
||||
);
|
||||
|
||||
const combinedExpression = useMemo(
|
||||
() => combineInitialAndUserExpression(initialExpression, committedExpression),
|
||||
[initialExpression, committedExpression],
|
||||
);
|
||||
|
||||
const contextValue = useMemo<QuerySearchV2ContextValue>(
|
||||
() => ({
|
||||
expression: combinedExpression,
|
||||
userExpression: committedExpression,
|
||||
initialExpression,
|
||||
querySearchProps: {
|
||||
initialExpression: initialExpression.trim() ? initialExpression : undefined,
|
||||
onChange: handleChange,
|
||||
onRun: handleRun,
|
||||
},
|
||||
}),
|
||||
[
|
||||
combinedExpression,
|
||||
committedExpression,
|
||||
initialExpression,
|
||||
handleChange,
|
||||
handleRun,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<QuerySearchV2Context.Provider value={contextValue}>
|
||||
{children}
|
||||
</QuerySearchV2Context.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { createStore, StoreApi } from 'zustand';
|
||||
|
||||
export type QuerySearchV2Store = {
|
||||
/**
|
||||
* User-typed expression (local state, updates on typing)
|
||||
*/
|
||||
inputExpression: string;
|
||||
/**
|
||||
* Committed expression (synced to URL, updates on submit)
|
||||
*/
|
||||
committedExpression: string;
|
||||
setInputExpression: (expression: string) => void;
|
||||
commitExpression: (expression: string) => void;
|
||||
resetExpression: () => void;
|
||||
initializeFromUrl: (urlExpression: string) => void;
|
||||
};
|
||||
|
||||
export interface QuerySearchProps {
|
||||
initialExpression: string | undefined;
|
||||
onChange: (expression: string) => void;
|
||||
onRun: (expression: string) => void;
|
||||
}
|
||||
|
||||
export interface QuerySearchV2ContextValue {
|
||||
/**
|
||||
* Combined expression: "initialExpression AND (userExpression)"
|
||||
*/
|
||||
expression: string;
|
||||
userExpression: string;
|
||||
initialExpression: string;
|
||||
querySearchProps: QuerySearchProps;
|
||||
}
|
||||
|
||||
export function createExpressionStore(): StoreApi<QuerySearchV2Store> {
|
||||
return createStore<QuerySearchV2Store>((set) => ({
|
||||
inputExpression: '',
|
||||
committedExpression: '',
|
||||
setInputExpression: (expression: string): void => {
|
||||
set({ inputExpression: expression });
|
||||
},
|
||||
commitExpression: (expression: string): void => {
|
||||
set({
|
||||
inputExpression: expression,
|
||||
committedExpression: expression,
|
||||
});
|
||||
},
|
||||
resetExpression: (): void => {
|
||||
set({
|
||||
inputExpression: '',
|
||||
committedExpression: '',
|
||||
});
|
||||
},
|
||||
initializeFromUrl: (urlExpression: string): void => {
|
||||
set({
|
||||
inputExpression: urlExpression,
|
||||
committedExpression: urlExpression,
|
||||
});
|
||||
},
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
|
||||
import { useQuerySearchV2Context } from '../context';
|
||||
import {
|
||||
QuerySearchV2Provider,
|
||||
QuerySearchV2ProviderProps,
|
||||
} from '../QuerySearchV2.provider';
|
||||
|
||||
const mockSetQueryState = jest.fn();
|
||||
let mockUrlValue: string | null = null;
|
||||
|
||||
jest.mock('nuqs', () => ({
|
||||
parseAsString: {},
|
||||
useQueryState: jest.fn(() => [mockUrlValue, mockSetQueryState]),
|
||||
}));
|
||||
|
||||
function createWrapper(
|
||||
props: Partial<QuerySearchV2ProviderProps> = {},
|
||||
): ({ children }: { children: ReactNode }) => JSX.Element {
|
||||
return function Wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
return (
|
||||
<QuerySearchV2Provider queryParamKey="testExpression" {...props}>
|
||||
{children}
|
||||
</QuerySearchV2Provider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
describe('QuerySearchExpressionProvider', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUrlValue = null;
|
||||
});
|
||||
|
||||
it('should provide initial context values', () => {
|
||||
const { result } = renderHook(() => useQuerySearchV2Context(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.expression).toBe('');
|
||||
expect(result.current.userExpression).toBe('');
|
||||
expect(result.current.initialExpression).toBe('');
|
||||
});
|
||||
|
||||
it('should combine initialExpression with userExpression', () => {
|
||||
const { result } = renderHook(() => useQuerySearchV2Context(), {
|
||||
wrapper: createWrapper({ initialExpression: 'k8s.pod.name = "my-pod"' }),
|
||||
});
|
||||
|
||||
expect(result.current.expression).toBe('k8s.pod.name = "my-pod"');
|
||||
expect(result.current.initialExpression).toBe('k8s.pod.name = "my-pod"');
|
||||
|
||||
act(() => {
|
||||
result.current.querySearchProps.onChange('service = "api"');
|
||||
});
|
||||
act(() => {
|
||||
result.current.querySearchProps.onRun('service = "api"');
|
||||
});
|
||||
|
||||
expect(result.current.expression).toBe(
|
||||
'k8s.pod.name = "my-pod" AND (service = "api")',
|
||||
);
|
||||
expect(result.current.userExpression).toBe('service = "api"');
|
||||
});
|
||||
|
||||
it('should provide querySearchProps with correct callbacks', () => {
|
||||
const { result } = renderHook(() => useQuerySearchV2Context(), {
|
||||
wrapper: createWrapper({ initialExpression: 'initial' }),
|
||||
});
|
||||
|
||||
expect(result.current.querySearchProps.initialExpression).toBe('initial');
|
||||
expect(typeof result.current.querySearchProps.onChange).toBe('function');
|
||||
expect(typeof result.current.querySearchProps.onRun).toBe('function');
|
||||
});
|
||||
|
||||
it('should initialize from URL value on mount', () => {
|
||||
mockUrlValue = 'status = 500';
|
||||
|
||||
const { result } = renderHook(() => useQuerySearchV2Context(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.userExpression).toBe('status = 500');
|
||||
expect(result.current.expression).toBe('status = 500');
|
||||
});
|
||||
|
||||
it('should throw error when used outside provider', () => {
|
||||
expect(() => {
|
||||
renderHook(() => useQuerySearchV2Context());
|
||||
}).toThrow(
|
||||
'useQuerySearchV2Context must be used within a QuerySearchV2Provider',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { createExpressionStore } from '../QuerySearchV2.store';
|
||||
|
||||
describe('createExpressionStore', () => {
|
||||
it('should create a store with initial state', () => {
|
||||
const store = createExpressionStore();
|
||||
const state = store.getState();
|
||||
|
||||
expect(state.inputExpression).toBe('');
|
||||
expect(state.committedExpression).toBe('');
|
||||
});
|
||||
|
||||
it('should update inputExpression via setInputExpression', () => {
|
||||
const store = createExpressionStore();
|
||||
|
||||
store.getState().setInputExpression('service.name = "api"');
|
||||
|
||||
expect(store.getState().inputExpression).toBe('service.name = "api"');
|
||||
expect(store.getState().committedExpression).toBe('');
|
||||
});
|
||||
|
||||
it('should update both expressions via commitExpression', () => {
|
||||
const store = createExpressionStore();
|
||||
|
||||
store.getState().setInputExpression('service.name = "api"');
|
||||
store.getState().commitExpression('service.name = "api"');
|
||||
|
||||
expect(store.getState().inputExpression).toBe('service.name = "api"');
|
||||
expect(store.getState().committedExpression).toBe('service.name = "api"');
|
||||
});
|
||||
|
||||
it('should reset all state via resetExpression', () => {
|
||||
const store = createExpressionStore();
|
||||
|
||||
store.getState().setInputExpression('service.name = "api"');
|
||||
store.getState().commitExpression('service.name = "api"');
|
||||
store.getState().resetExpression();
|
||||
|
||||
expect(store.getState().inputExpression).toBe('');
|
||||
expect(store.getState().committedExpression).toBe('');
|
||||
});
|
||||
|
||||
it('should initialize from URL value', () => {
|
||||
const store = createExpressionStore();
|
||||
|
||||
store.getState().initializeFromUrl('status = 500');
|
||||
|
||||
expect(store.getState().inputExpression).toBe('status = 500');
|
||||
expect(store.getState().committedExpression).toBe('status = 500');
|
||||
});
|
||||
|
||||
it('should create isolated store instances', () => {
|
||||
const store1 = createExpressionStore();
|
||||
const store2 = createExpressionStore();
|
||||
|
||||
store1.getState().setInputExpression('expr1');
|
||||
store2.getState().setInputExpression('expr2');
|
||||
|
||||
expect(store1.getState().inputExpression).toBe('expr1');
|
||||
expect(store2.getState().inputExpression).toBe('expr2');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
// eslint-disable-next-line no-restricted-imports -- React Context required for scoped store pattern
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
import type { QuerySearchV2ContextValue } from './QuerySearchV2.store';
|
||||
|
||||
export const QuerySearchV2Context =
|
||||
createContext<QuerySearchV2ContextValue | null>(null);
|
||||
|
||||
export function useQuerySearchV2Context(): QuerySearchV2ContextValue {
|
||||
const context = useContext(QuerySearchV2Context);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useQuerySearchV2Context must be used within a QuerySearchV2Provider',
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export { useQuerySearchV2Context } from './context';
|
||||
export type { QuerySearchV2ProviderProps } from './QuerySearchV2.provider';
|
||||
export { QuerySearchV2Provider } from './QuerySearchV2.provider';
|
||||
export type {
|
||||
QuerySearchProps,
|
||||
QuerySearchV2ContextValue,
|
||||
QuerySearchV2Store,
|
||||
} from './QuerySearchV2.store';
|
||||
@@ -19,6 +19,13 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.query-search-initial-scope-label {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.query-where-clause-editor {
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
@@ -53,6 +60,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hasInitialExpression .cm-editor .cm-content {
|
||||
padding-left: 22px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
@@ -68,7 +79,6 @@
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
padding: 0px !important;
|
||||
background-color: var(--l1-background) !important;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--l1-border);
|
||||
|
||||
@@ -30,7 +30,7 @@ import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariabl
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { debounce, isNull } from 'lodash-es';
|
||||
import { Info, TriangleAlert } from 'lucide-react';
|
||||
import { Filter, Info, TriangleAlert } from 'lucide-react';
|
||||
import {
|
||||
IDetailedError,
|
||||
IQueryContext,
|
||||
@@ -47,6 +47,7 @@ import { validateQuery } from 'utils/queryValidationUtils';
|
||||
import { unquote } from 'utils/stringUtils';
|
||||
|
||||
import { queryExamples } from './constants';
|
||||
import { combineInitialAndUserExpression } from './utils';
|
||||
|
||||
import './QuerySearch.styles.scss';
|
||||
|
||||
@@ -85,6 +86,8 @@ interface QuerySearchProps {
|
||||
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
|
||||
onRun?: (query: string) => void;
|
||||
showFilterSuggestionsWithoutMetric?: boolean;
|
||||
/** When set, the editor shows only the user expression; API/filter uses `initial AND (user)`. */
|
||||
initialExpression?: string;
|
||||
}
|
||||
|
||||
function QuerySearch({
|
||||
@@ -96,6 +99,7 @@ function QuerySearch({
|
||||
signalSource,
|
||||
hardcodedAttributeKeys,
|
||||
showFilterSuggestionsWithoutMetric,
|
||||
initialExpression,
|
||||
}: QuerySearchProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
|
||||
@@ -112,18 +116,26 @@ function QuerySearch({
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const editorRef = useRef<EditorView | null>(null);
|
||||
|
||||
const handleQueryValidation = useCallback((newExpression: string): void => {
|
||||
try {
|
||||
const validationResponse = validateQuery(newExpression);
|
||||
setValidation(validationResponse);
|
||||
} catch (error) {
|
||||
setValidation({
|
||||
isValid: false,
|
||||
message: 'Failed to process query',
|
||||
errors: [error as IDetailedError],
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
const isScopedFilter = initialExpression !== undefined;
|
||||
|
||||
const validateExpressionForEditor = useCallback(
|
||||
(editorDoc: string): void => {
|
||||
const toValidate = isScopedFilter
|
||||
? combineInitialAndUserExpression(initialExpression ?? '', editorDoc)
|
||||
: editorDoc;
|
||||
try {
|
||||
const validationResponse = validateQuery(toValidate);
|
||||
setValidation(validationResponse);
|
||||
} catch (error) {
|
||||
setValidation({
|
||||
isValid: false,
|
||||
message: 'Failed to process query',
|
||||
errors: [error as IDetailedError],
|
||||
});
|
||||
}
|
||||
},
|
||||
[initialExpression, isScopedFilter],
|
||||
);
|
||||
|
||||
const getCurrentExpression = useCallback(
|
||||
(): string => editorRef.current?.state.doc.toString() || '',
|
||||
@@ -165,6 +177,8 @@ function QuerySearch({
|
||||
setIsEditorReady(true);
|
||||
}, []);
|
||||
|
||||
const prevQueryDataExpressionRef = useRef<string | undefined>();
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!isEditorReady) {
|
||||
@@ -173,13 +187,22 @@ function QuerySearch({
|
||||
|
||||
const newExpression = queryData.filter?.expression || '';
|
||||
const currentExpression = getCurrentExpression();
|
||||
const prevExpression = prevQueryDataExpressionRef.current;
|
||||
|
||||
// Do not update codemirror editor if the expression is the same
|
||||
if (newExpression !== currentExpression && !isFocused) {
|
||||
// Only sync editor when queryData.filter?.expression actually changed from external source
|
||||
// Not when focus changed (which would reset uncommitted user input)
|
||||
const queryDataExpressionChanged = prevExpression !== newExpression;
|
||||
prevQueryDataExpressionRef.current = newExpression;
|
||||
|
||||
if (
|
||||
queryDataExpressionChanged &&
|
||||
newExpression !== currentExpression &&
|
||||
!isFocused
|
||||
) {
|
||||
updateEditorValue(newExpression, { skipOnChange: true });
|
||||
if (newExpression) {
|
||||
handleQueryValidation(newExpression);
|
||||
}
|
||||
}
|
||||
if (!isFocused) {
|
||||
validateExpressionForEditor(currentExpression);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -284,7 +307,7 @@ function QuerySearch({
|
||||
}
|
||||
});
|
||||
}
|
||||
setKeySuggestions(Array.from(merged.values()));
|
||||
setKeySuggestions([...merged.values()]);
|
||||
|
||||
// Force reopen the completion if editor is available and focused
|
||||
if (editorRef.current) {
|
||||
@@ -337,7 +360,7 @@ function QuerySearch({
|
||||
// If value contains single quotes, escape them and wrap in single quotes
|
||||
if (value.includes("'")) {
|
||||
// Replace single quotes with escaped single quotes
|
||||
const escapedValue = value.replace(/'/g, "\\'");
|
||||
const escapedValue = value.replaceAll(/'/g, "\\'");
|
||||
return `'${escapedValue}'`;
|
||||
}
|
||||
|
||||
@@ -614,7 +637,7 @@ function QuerySearch({
|
||||
|
||||
const handleBlur = (): void => {
|
||||
const currentExpression = getCurrentExpression();
|
||||
handleQueryValidation(currentExpression);
|
||||
validateExpressionForEditor(currentExpression);
|
||||
setIsFocused(false);
|
||||
};
|
||||
|
||||
@@ -632,7 +655,6 @@ function QuerySearch({
|
||||
);
|
||||
|
||||
const handleExampleClick = (exampleQuery: string): void => {
|
||||
// If there's an existing query, append the example with AND
|
||||
const currentExpression = getCurrentExpression();
|
||||
const newExpression = currentExpression
|
||||
? `${currentExpression} AND ${exampleQuery}`
|
||||
@@ -897,12 +919,12 @@ function QuerySearch({
|
||||
|
||||
// If we have previous pairs, we can prioritize keys that haven't been used yet
|
||||
if (queryContext.queryPairs && queryContext.queryPairs.length > 0) {
|
||||
const usedKeys = queryContext.queryPairs.map((pair) => pair.key);
|
||||
const usedKeys = new Set(queryContext.queryPairs.map((pair) => pair.key));
|
||||
|
||||
// Add boost to unused keys to prioritize them
|
||||
options = options.map((option) => ({
|
||||
...option,
|
||||
boost: usedKeys.includes(option.label) ? -10 : 10,
|
||||
boost: usedKeys.has(option.label) ? -10 : 10,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1317,6 +1339,19 @@ function QuerySearch({
|
||||
)}
|
||||
|
||||
<div className="query-where-clause-editor-container">
|
||||
{isScopedFilter ? (
|
||||
<Tooltip title={initialExpression || ''} placement="left">
|
||||
<div className="query-search-initial-scope-label">
|
||||
<Filter
|
||||
size={14}
|
||||
style={{
|
||||
opacity: 0.9,
|
||||
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
<Tooltip
|
||||
title={<div data-log-detail-ignore="true">{getTooltipContent()}</div>}
|
||||
placement="left"
|
||||
@@ -1356,6 +1391,7 @@ function QuerySearch({
|
||||
className={cx('query-where-clause-editor', {
|
||||
isValid: validation.isValid === true,
|
||||
hasErrors: validation.errors.length > 0,
|
||||
hasInitialExpression: isScopedFilter,
|
||||
})}
|
||||
extensions={[
|
||||
autocompletion({
|
||||
@@ -1390,7 +1426,12 @@ function QuerySearch({
|
||||
// Mod-Enter is usually Ctrl-Enter or Cmd-Enter based on OS
|
||||
run: (): boolean => {
|
||||
if (onRun && typeof onRun === 'function') {
|
||||
onRun(getCurrentExpression());
|
||||
const user = getCurrentExpression();
|
||||
onRun(
|
||||
isScopedFilter
|
||||
? combineInitialAndUserExpression(initialExpression ?? '', user)
|
||||
: user,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@@ -1555,6 +1596,7 @@ QuerySearch.defaultProps = {
|
||||
placeholder:
|
||||
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')",
|
||||
showFilterSuggestionsWithoutMetric: false,
|
||||
initialExpression: undefined,
|
||||
};
|
||||
|
||||
export default QuerySearch;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
getUserExpressionFromCombined,
|
||||
} from '../utils';
|
||||
|
||||
describe('entityLogsExpression', () => {
|
||||
describe('combineInitialAndUserExpression', () => {
|
||||
it('returns user when initial is empty', () => {
|
||||
expect(combineInitialAndUserExpression('', 'body contains error')).toBe(
|
||||
'body contains error',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns initial when user is empty', () => {
|
||||
expect(combineInitialAndUserExpression('k8s.pod.name = "x"', '')).toBe(
|
||||
'k8s.pod.name = "x"',
|
||||
);
|
||||
});
|
||||
|
||||
it('wraps user in parentheses with AND', () => {
|
||||
expect(
|
||||
combineInitialAndUserExpression('k8s.pod.name = "x"', 'body = "a"'),
|
||||
).toBe('k8s.pod.name = "x" AND (body = "a")');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserExpressionFromCombined', () => {
|
||||
it('returns empty when combined equals initial', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined('k8s.pod.name = "x"', 'k8s.pod.name = "x"'),
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
it('extracts user from wrapped form', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined(
|
||||
'k8s.pod.name = "x"',
|
||||
'k8s.pod.name = "x" AND (body = "a")',
|
||||
),
|
||||
).toBe('body = "a"');
|
||||
});
|
||||
|
||||
it('extracts user from legacy AND without parens', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined(
|
||||
'k8s.pod.name = "x"',
|
||||
'k8s.pod.name = "x" AND body = "a"',
|
||||
),
|
||||
).toBe('body = "a"');
|
||||
});
|
||||
|
||||
it('returns full combined when initial is empty', () => {
|
||||
expect(getUserExpressionFromCombined('', 'service.name = "a"')).toBe(
|
||||
'service.name = "a"',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
export function combineInitialAndUserExpression(
|
||||
initial: string,
|
||||
user: string,
|
||||
): string {
|
||||
const i = initial.trim();
|
||||
const u = user.trim();
|
||||
if (!i) {
|
||||
return u;
|
||||
}
|
||||
if (!u) {
|
||||
return i;
|
||||
}
|
||||
return `${i} AND (${u})`;
|
||||
}
|
||||
|
||||
export function getUserExpressionFromCombined(
|
||||
initial: string,
|
||||
combined: string | null | undefined,
|
||||
): string {
|
||||
const i = initial.trim();
|
||||
const c = (combined ?? '').trim();
|
||||
if (!c) {
|
||||
return '';
|
||||
}
|
||||
if (!i) {
|
||||
return c;
|
||||
}
|
||||
if (c === i) {
|
||||
return '';
|
||||
}
|
||||
const wrappedPrefix = `${i} AND (`;
|
||||
if (c.startsWith(wrappedPrefix) && c.endsWith(')')) {
|
||||
return c.slice(wrappedPrefix.length, -1);
|
||||
}
|
||||
const plainPrefix = `${i} AND `;
|
||||
if (c.startsWith(plainPrefix)) {
|
||||
return c.slice(plainPrefix.length);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
14
frontend/src/components/QueryBuilderV2/index.ts
Normal file
14
frontend/src/components/QueryBuilderV2/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type {
|
||||
QuerySearchProps,
|
||||
QuerySearchV2ContextValue,
|
||||
QuerySearchV2ProviderProps,
|
||||
} from './QueryV2/QuerySearch/Provider';
|
||||
export {
|
||||
QuerySearchV2Provider,
|
||||
useQuerySearchV2Context,
|
||||
} from './QueryV2/QuerySearch/Provider';
|
||||
export { QueryBuilderV2 } from './QueryBuilderV2';
|
||||
export {
|
||||
QueryBuilderV2Provider,
|
||||
useQueryBuilderV2Context,
|
||||
} from './QueryBuilderV2Context';
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
input {
|
||||
color: var(--l1-foreground);
|
||||
font-size: var(--font-size-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.ant-picker-suffix {
|
||||
@@ -126,12 +126,6 @@
|
||||
}
|
||||
|
||||
&__copy-btn {
|
||||
flex-shrink: 0;
|
||||
height: 32px;
|
||||
border-radius: 0 2px 2px 0;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
border-left: 1px solid var(--l1-border);
|
||||
min-width: 40px;
|
||||
}
|
||||
@@ -152,6 +146,7 @@
|
||||
color: var(--foreground);
|
||||
letter-spacing: 0.48px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
&__footer {
|
||||
|
||||
@@ -22,9 +22,8 @@ function KeyCreatedPhase({
|
||||
<div className="add-key-modal__key-display">
|
||||
<span className="add-key-modal__key-text">{createdKey.key}</span>
|
||||
<Button
|
||||
variant="outlined"
|
||||
variant="link"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={onCopy}
|
||||
className="add-key-modal__copy-btn"
|
||||
>
|
||||
|
||||
@@ -106,7 +106,7 @@ function KeyFormPhase({
|
||||
|
||||
<div className="add-key-modal__footer">
|
||||
<div className="add-key-modal__footer-right">
|
||||
<Button variant="solid" color="secondary" size="sm" onClick={onClose}>
|
||||
<Button variant="solid" color="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -115,7 +115,6 @@ function KeyFormPhase({
|
||||
form={FORM_ID}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="sm"
|
||||
loading={isSubmitting}
|
||||
disabled={!isValid}
|
||||
>
|
||||
|
||||
@@ -136,7 +136,7 @@ function EditKeyForm({
|
||||
</form>
|
||||
|
||||
<div className="edit-key-modal__footer">
|
||||
<Button variant="ghost" color="destructive" onClick={onRevokeClick}>
|
||||
<Button variant="link" color="destructive" onClick={onRevokeClick}>
|
||||
<Trash2 size={12} />
|
||||
Revoke Key
|
||||
</Button>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
|
||||
input {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 13px;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.ant-picker-suffix {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { RevokeKeyContent } from '../RevokeKeyModal';
|
||||
import { RevokeKeyFooter } from '../RevokeKeyModal';
|
||||
import EditKeyForm from './EditKeyForm';
|
||||
import type { FormValues } from './types';
|
||||
import { DEFAULT_FORM_VALUES, ExpiryMode } from './types';
|
||||
@@ -158,17 +158,25 @@ function EditKeyModal({ keyItem }: EditKeyModalProps): JSX.Element {
|
||||
}
|
||||
width={isRevokeConfirmOpen ? 'narrow' : 'base'}
|
||||
className={
|
||||
isRevokeConfirmOpen ? 'alert-dialog delete-dialog' : 'edit-key-modal'
|
||||
isRevokeConfirmOpen ? 'alert-dialog sa-delete-dialog' : 'edit-key-modal'
|
||||
}
|
||||
showCloseButton={!isRevokeConfirmOpen}
|
||||
disableOutsideClick={isErrorModalVisible}
|
||||
footer={
|
||||
isRevokeConfirmOpen ? (
|
||||
<RevokeKeyFooter
|
||||
isRevoking={isRevoking}
|
||||
onCancel={(): void => setIsRevokeConfirmOpen(false)}
|
||||
onConfirm={handleRevoke}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{isRevokeConfirmOpen ? (
|
||||
<RevokeKeyContent
|
||||
isRevoking={isRevoking}
|
||||
onCancel={(): void => setIsRevokeConfirmOpen(false)}
|
||||
onConfirm={handleRevoke}
|
||||
/>
|
||||
<>
|
||||
Revoking this key will permanently invalidate it. Any systems using this
|
||||
key will lose access immediately.
|
||||
</>
|
||||
) : (
|
||||
<EditKeyForm
|
||||
register={register}
|
||||
|
||||
@@ -72,7 +72,6 @@ function OverviewTab({
|
||||
id="sa-name"
|
||||
value={localName}
|
||||
onChange={(e): void => onNameChange(e.target.value)}
|
||||
className="sa-drawer__input"
|
||||
placeholder="Enter name"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -17,39 +17,32 @@ import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
export interface RevokeKeyContentProps {
|
||||
export interface RevokeKeyFooterProps {
|
||||
isRevoking: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export function RevokeKeyContent({
|
||||
export function RevokeKeyFooter({
|
||||
isRevoking,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: RevokeKeyContentProps): JSX.Element {
|
||||
}: RevokeKeyFooterProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<p className="delete-dialog__body">
|
||||
Revoking this key will permanently invalidate it. Any systems using this key
|
||||
will lose access immediately.
|
||||
</p>
|
||||
<div className="delete-dialog__footer">
|
||||
<Button variant="solid" color="secondary" size="sm" onClick={onCancel}>
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
size="sm"
|
||||
loading={isRevoking}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Revoke Key
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="solid" color="secondary" onClick={onCancel}>
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
loading={isRevoking}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Revoke Key
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -112,15 +105,19 @@ function RevokeKeyModal(): JSX.Element {
|
||||
}}
|
||||
title={`Revoke ${keyName ?? 'key'}?`}
|
||||
width="narrow"
|
||||
className="alert-dialog delete-dialog"
|
||||
className="alert-dialog sa-delete-dialog"
|
||||
showCloseButton={false}
|
||||
disableOutsideClick={isErrorModalVisible}
|
||||
footer={
|
||||
<RevokeKeyFooter
|
||||
isRevoking={isRevoking}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RevokeKeyContent
|
||||
isRevoking={isRevoking}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
Revoking this key will permanently invalidate it. Any systems using this key
|
||||
will lose access immediately.
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
&__tab {
|
||||
@@ -166,18 +168,6 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&__input {
|
||||
height: 32px;
|
||||
background: var(--l2-background);
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
box-shadow: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -186,7 +176,7 @@
|
||||
padding: 0 var(--padding-2);
|
||||
border-radius: 2px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
border: 1px solid var(--border);
|
||||
|
||||
&--disabled {
|
||||
cursor: not-allowed;
|
||||
@@ -195,8 +185,8 @@
|
||||
}
|
||||
|
||||
&__input-text {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
font-weight: var(--paragraph-base-400-font-weight);
|
||||
color: var(--foreground);
|
||||
line-height: var(--line-height-18);
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
@@ -129,7 +129,7 @@ function ServiceAccountDrawer({
|
||||
useEffect(() => {
|
||||
if (account?.id) {
|
||||
setLocalName(account?.name ?? '');
|
||||
setKeysPage(1);
|
||||
void setKeysPage(1);
|
||||
}
|
||||
}, [account?.id, account?.name, setKeysPage]);
|
||||
|
||||
@@ -176,7 +176,7 @@ function ServiceAccountDrawer({
|
||||
}
|
||||
const maxPage = Math.max(1, Math.ceil(keys.length / PAGE_SIZE));
|
||||
if (keysPage > maxPage) {
|
||||
setKeysPage(maxPage);
|
||||
void setKeysPage(maxPage);
|
||||
}
|
||||
}, [keysLoading, keys.length, keysPage, setKeysPage]);
|
||||
|
||||
@@ -214,8 +214,8 @@ function ServiceAccountDrawer({
|
||||
data: { name: localName },
|
||||
});
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Name update'));
|
||||
refetchAccount();
|
||||
queryClient.invalidateQueries(getListServiceAccountsQueryKey());
|
||||
void refetchAccount();
|
||||
void queryClient.invalidateQueries(getListServiceAccountsQueryKey());
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
@@ -337,8 +337,8 @@ function ServiceAccountDrawer({
|
||||
onSuccess({ closeDrawer: false });
|
||||
}
|
||||
|
||||
refetchAccount();
|
||||
queryClient.invalidateQueries(getListServiceAccountsQueryKey());
|
||||
void refetchAccount();
|
||||
void queryClient.invalidateQueries(getListServiceAccountsQueryKey());
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -357,12 +357,12 @@ function ServiceAccountDrawer({
|
||||
]);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
setIsDeleteOpen(null);
|
||||
setIsAddKeyOpen(null);
|
||||
setSelectedAccountId(null);
|
||||
setActiveTab(null);
|
||||
setKeysPage(null);
|
||||
setEditKeyId(null);
|
||||
void setIsDeleteOpen(null);
|
||||
void setIsAddKeyOpen(null);
|
||||
void setSelectedAccountId(null);
|
||||
void setActiveTab(null);
|
||||
void setKeysPage(null);
|
||||
void setEditKeyId(null);
|
||||
setSaveErrors([]);
|
||||
}, [
|
||||
setSelectedAccountId,
|
||||
@@ -379,12 +379,13 @@ function ServiceAccountDrawer({
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={activeTab}
|
||||
size="sm"
|
||||
onChange={(val): void => {
|
||||
if (val) {
|
||||
setActiveTab(val as ServiceAccountDrawerTab);
|
||||
void setActiveTab(val as ServiceAccountDrawerTab);
|
||||
if (val !== ServiceAccountDrawerTab.Keys) {
|
||||
setKeysPage(null);
|
||||
setEditKeyId(null);
|
||||
void setKeysPage(null);
|
||||
void setEditKeyId(null);
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -415,7 +416,7 @@ function ServiceAccountDrawer({
|
||||
color="secondary"
|
||||
disabled={isDeleted}
|
||||
onClick={(): void => {
|
||||
setIsAddKeyOpen(true);
|
||||
void setIsAddKeyOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus size={12} />
|
||||
@@ -503,7 +504,7 @@ function ServiceAccountDrawer({
|
||||
variant="link"
|
||||
color="destructive"
|
||||
onClick={(): void => {
|
||||
setIsDeleteOpen(true);
|
||||
void setIsDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
@@ -512,7 +513,7 @@ function ServiceAccountDrawer({
|
||||
)}
|
||||
{!isDeleted && (
|
||||
<div className="sa-drawer__footer-right">
|
||||
<Button variant="solid" color="secondary" onClick={handleClose}>
|
||||
<Button variant="outlined" color="secondary" onClick={handleClose}>
|
||||
<X size={14} />
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { css, FlattenSimpleInterpolation } from 'styled-components';
|
||||
|
||||
const cssProperty = (key: any, value: any): FlattenSimpleInterpolation =>
|
||||
key &&
|
||||
value &&
|
||||
css`
|
||||
${key}: ${value};
|
||||
`;
|
||||
|
||||
interface IFlexProps {
|
||||
flexDirection?: string; // Need to replace this with exact css props. Not able to find any :(
|
||||
flex?: number | string;
|
||||
}
|
||||
export const Flex = ({
|
||||
flexDirection,
|
||||
flex,
|
||||
}: IFlexProps): FlattenSimpleInterpolation => css`
|
||||
${cssProperty('flex-direction', flexDirection)}
|
||||
${cssProperty('flex', flex)}
|
||||
`;
|
||||
|
||||
interface IDisplayProps {
|
||||
display?: string;
|
||||
}
|
||||
export const Display = ({
|
||||
display,
|
||||
}: IDisplayProps): FlattenSimpleInterpolation => css`
|
||||
${cssProperty('display', display)}
|
||||
`;
|
||||
|
||||
interface ISpacingProps {
|
||||
margin?: string;
|
||||
padding?: string;
|
||||
}
|
||||
export const Spacing = ({
|
||||
margin,
|
||||
padding,
|
||||
}: ISpacingProps): FlattenSimpleInterpolation => css`
|
||||
${cssProperty('margin', margin)}
|
||||
${cssProperty('padding', padding)}
|
||||
`;
|
||||
@@ -1,5 +0,0 @@
|
||||
export type TabLabelProps = {
|
||||
isDisabled: boolean;
|
||||
label: string;
|
||||
tooltipText?: string;
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
|
||||
import { TabLabelProps } from './TabLabel.interfaces';
|
||||
|
||||
function TabLabel({
|
||||
label,
|
||||
isDisabled,
|
||||
tooltipText,
|
||||
}: TabLabelProps): JSX.Element {
|
||||
const currentLabel = <span data-testid={`${label}`}>{label}</span>;
|
||||
|
||||
if (isDisabled) {
|
||||
return (
|
||||
<Tooltip
|
||||
trigger="hover"
|
||||
autoAdjustOverflow
|
||||
placement="top"
|
||||
title={tooltipText}
|
||||
>
|
||||
{currentLabel}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return currentLabel;
|
||||
}
|
||||
|
||||
export default memo(TabLabel);
|
||||
@@ -1,5 +0,0 @@
|
||||
.tab-title {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Radio } from 'antd';
|
||||
import type { RadioChangeEvent } from 'antd/lib';
|
||||
import { History, Table } from 'lucide-react';
|
||||
|
||||
import { ALERT_TABS } from '../constants';
|
||||
|
||||
import './Tabs.styles.scss';
|
||||
|
||||
export function Tabs(): JSX.Element {
|
||||
const [selectedTab, setSelectedTab] = useState('overview');
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedTab(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Radio.Group className="tabs" onChange={handleTabChange} value={selectedTab}>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedTab === ALERT_TABS.OVERVIEW ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={ALERT_TABS.OVERVIEW}
|
||||
>
|
||||
<div className="tab-title">
|
||||
<Table size={14} />
|
||||
Overview
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={selectedTab === ALERT_TABS.HISTORY ? 'selected_view tab' : 'tab'}
|
||||
value={ALERT_TABS.HISTORY}
|
||||
>
|
||||
<div className="tab-title">
|
||||
<History size={14} />
|
||||
History
|
||||
</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
@mixin flex-center {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tabs-and-filters {
|
||||
@include flex-center;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
.filters {
|
||||
@include flex-center;
|
||||
gap: 16px;
|
||||
.reset-button {
|
||||
@include flex-center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Filters } from 'components/AlertDetailsFilters/Filters';
|
||||
|
||||
import { Tabs } from './Tabs/Tabs';
|
||||
|
||||
import './TabsAndFilters.styles.scss';
|
||||
|
||||
function TabsAndFilters(): JSX.Element {
|
||||
return (
|
||||
<div className="tabs-and-filters">
|
||||
<Tabs />
|
||||
<Filters />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TabsAndFilters;
|
||||
@@ -1,5 +0,0 @@
|
||||
export const ALERT_TABS = {
|
||||
OVERVIEW: 'OVERVIEW',
|
||||
HISTORY: 'HISTORY',
|
||||
ACTIVITY: 'ACTIVITY',
|
||||
} as const;
|
||||
@@ -47,10 +47,16 @@ function TanStackCustomTableRow<TData>({
|
||||
const isActive = context?.isRowActive?.(rowData) ?? false;
|
||||
const extraClass = context?.getRowClassName?.(rowData) ?? '';
|
||||
const rowStyle = context?.getRowStyle?.(rowData);
|
||||
const enableAlternatingRowColors =
|
||||
context?.enableAlternatingRowColors ?? false;
|
||||
|
||||
const rowClassName = cx(
|
||||
tableStyles.tableRow,
|
||||
isActive && tableStyles.tableRowActive,
|
||||
enableAlternatingRowColors &&
|
||||
(item.row.index % 2 === 0
|
||||
? tableStyles.tableRowEven
|
||||
: tableStyles.tableRowOdd),
|
||||
extraClass,
|
||||
);
|
||||
|
||||
@@ -105,6 +111,12 @@ function areTableRowPropsEqual<TData>(
|
||||
if (prev.context?.columnVisibilityKey !== next.context?.columnVisibilityKey) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
prev.context?.enableAlternatingRowColors !==
|
||||
next.context?.enableAlternatingRowColors
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prev.context !== next.context) {
|
||||
const prevActive = prev.context?.isRowActive?.(prevData) ?? false;
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
padding: 0.3rem;
|
||||
padding: var(--tanstack-cell-padding-top, 0.3rem)
|
||||
var(--tanstack-cell-padding-right, 0.3rem)
|
||||
var(--tanstack-cell-padding-bottom, 0.3rem)
|
||||
var(--tanstack-cell-padding-left, 0.3rem);
|
||||
transform: translate3d(
|
||||
var(--tanstack-header-translate-x, 0px),
|
||||
var(--tanstack-header-translate-y, 0px),
|
||||
@@ -19,7 +22,17 @@
|
||||
}
|
||||
|
||||
border: none !important;
|
||||
background-color: var(--l2-background) !important;
|
||||
background-color: var(
|
||||
--tanstack-table-header-cell-bg,
|
||||
var(--l2-background)
|
||||
) !important;
|
||||
|
||||
&:first-child {
|
||||
background-color: var(
|
||||
--tanstack-first-column-header-bg,
|
||||
var(--tanstack-table-header-cell-bg, var(--l2-background))
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tanstackHeaderContent {
|
||||
@@ -61,7 +74,7 @@
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
cursor: grab;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--tanstack-table-header-cell-color, var(--l2-foreground));
|
||||
opacity: 1;
|
||||
touch-action: none;
|
||||
}
|
||||
@@ -74,7 +87,7 @@
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--tanstack-table-header-cell-color, var(--l2-foreground));
|
||||
|
||||
margin-left: auto;
|
||||
}
|
||||
@@ -82,8 +95,9 @@
|
||||
.tanstackColumnActionsContent {
|
||||
width: 140px;
|
||||
padding: 0;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--tanstack-table-header-cell-bg, var(--l2-background));
|
||||
border: 1px solid
|
||||
var(--tanstack-table-header-cell-actions-border-color, var(--l2-border));
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -137,7 +151,7 @@
|
||||
}
|
||||
|
||||
.tanstackHeaderCell.isResizing .cursorColResize {
|
||||
background: var(--bg-robin-300);
|
||||
background: var(--tanstack-table-resize-active-bg, var(--bg-robin-300));
|
||||
}
|
||||
|
||||
.tanstackResizeHandleLine {
|
||||
@@ -147,7 +161,7 @@
|
||||
left: 50%;
|
||||
width: 4px;
|
||||
transform: translateX(-50%);
|
||||
background: var(--l2-background);
|
||||
background: var(--tanstack-table-resize-handle-bg, var(--l2-background));
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
transition:
|
||||
@@ -155,13 +169,34 @@
|
||||
width 120ms ease;
|
||||
}
|
||||
|
||||
.tanstackHeaderCell:first-child .tanstackResizeHandleLine {
|
||||
background: var(
|
||||
--tanstack-first-column-header-bg,
|
||||
var(--tanstack-table-resize-handle-bg, var(--l2-background))
|
||||
);
|
||||
}
|
||||
|
||||
.cursorColResize:hover .tanstackResizeHandleLine {
|
||||
background: var(--l2-border);
|
||||
background: var(--tanstack-table-resize-handle-hover-bg, var(--l2-border));
|
||||
}
|
||||
|
||||
.tanstackHeaderCell:first-child
|
||||
.cursorColResize:hover
|
||||
.tanstackResizeHandleLine {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(
|
||||
--tanstack-first-column-header-bg,
|
||||
var(--tanstack-table-resize-handle-bg, var(--l2-background))
|
||||
)
|
||||
60%,
|
||||
black
|
||||
);
|
||||
}
|
||||
|
||||
.tanstackHeaderCell.isResizing .tanstackResizeHandleLine {
|
||||
width: 2px;
|
||||
background: var(--bg-robin-500);
|
||||
background: var(--tanstack-table-resize-handle-active-bg, var(--bg-robin-500));
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -213,7 +248,12 @@
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--l3-foreground);
|
||||
|
||||
&[data-sort-direction='asc'],
|
||||
&[data-sort-direction='desc'] {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.isSortable {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useSortable } from '@dnd-kit/sortable';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui';
|
||||
import { flexRender, Header as TanStackHeader } from '@tanstack/react-table';
|
||||
import cx from 'classnames';
|
||||
import { ChevronDown, ChevronUp, GripVertical } from 'lucide-react';
|
||||
import { ArrowDown, ArrowUp, ArrowUpDown, GripVertical } from 'lucide-react';
|
||||
|
||||
import { SortState, TableColumnDef } from './types';
|
||||
|
||||
@@ -177,12 +177,17 @@ function TanStackHeaderRow<TData>({
|
||||
? column.header()
|
||||
: String(column.header || '').replace(/^\w/, (c) => c.toUpperCase())}
|
||||
</span>
|
||||
<span className={headerStyles.tanstackSortIndicator}>
|
||||
<span
|
||||
className={headerStyles.tanstackSortIndicator}
|
||||
data-sort-direction={currentSortDirection || 'none'}
|
||||
>
|
||||
{currentSortDirection === 'asc' ? (
|
||||
<ChevronUp size={SORT_ICON_SIZE} />
|
||||
<ArrowUp size={SORT_ICON_SIZE} />
|
||||
) : currentSortDirection === 'desc' ? (
|
||||
<ChevronDown size={SORT_ICON_SIZE} />
|
||||
) : null}
|
||||
<ArrowDown size={SORT_ICON_SIZE} />
|
||||
) : (
|
||||
<ArrowUpDown size={SORT_ICON_SIZE} />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
.tanStackTable {
|
||||
--tanstack-cell-padding: var(--tanstack-cell-padding-override, 0.3rem);
|
||||
--tanstack-cell-padding-left: var(
|
||||
--tanstack-cell-padding-left-override,
|
||||
var(--tanstack-cell-padding)
|
||||
);
|
||||
--tanstack-cell-padding-right: var(
|
||||
--tanstack-cell-padding-right-override,
|
||||
var(--tanstack-cell-padding)
|
||||
);
|
||||
--tanstack-cell-padding-top: var(
|
||||
--tanstack-cell-padding-top-override,
|
||||
var(--tanstack-cell-padding)
|
||||
);
|
||||
--tanstack-cell-padding-bottom: var(
|
||||
--tanstack-cell-padding-bottom-override,
|
||||
var(--tanstack-cell-padding)
|
||||
);
|
||||
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
@@ -26,7 +44,7 @@
|
||||
line-clamp: var(--tanstack-plain-body-line-clamp, 1);
|
||||
font-size: var(--tanstack-plain-cell-font-size, 14px);
|
||||
line-height: var(--tanstack-plain-cell-line-height, 18px);
|
||||
color: var(--l2-foreground);
|
||||
color: var(--tanstack-table-cell-color, var(--l2-foreground));
|
||||
max-width: 100%;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -42,13 +60,35 @@
|
||||
}
|
||||
|
||||
.tableCell {
|
||||
padding: 0.3rem;
|
||||
padding: var(--tanstack-cell-padding-top) var(--tanstack-cell-padding-right)
|
||||
var(--tanstack-cell-padding-bottom) var(--tanstack-cell-padding-left);
|
||||
height: var(--tanstack-table-row-height, auto);
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.07px;
|
||||
font-size: var(--tanstack-plain-cell-font-size, 14px);
|
||||
line-height: var(--tanstack-plain-cell-line-height, 18px);
|
||||
color: var(--l2-foreground);
|
||||
color: var(--tanstack-table-cell-color, var(--l2-foreground));
|
||||
background-color: var(--tanstack-table-cell-bg, transparent);
|
||||
|
||||
&:first-child {
|
||||
padding: var(
|
||||
--tanstack-cell-padding-top-first-column,
|
||||
var(--tanstack-cell-padding-top)
|
||||
)
|
||||
var(
|
||||
--tanstack-cell-padding-right-first-column,
|
||||
var(--tanstack-cell-padding-right)
|
||||
)
|
||||
var(
|
||||
--tanstack-cell-padding-bottom-first-column,
|
||||
var(--tanstack-cell-padding-bottom)
|
||||
)
|
||||
var(
|
||||
--tanstack-cell-padding-left-first-column,
|
||||
var(--tanstack-cell-padding-left)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.tableRow {
|
||||
@@ -58,19 +98,69 @@
|
||||
|
||||
&:hover {
|
||||
.tableCell {
|
||||
background-color: var(--row-hover-bg) !important;
|
||||
background-color: var(
|
||||
--tanstack-table-row-hover-bg,
|
||||
var(--row-hover-bg)
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.tableRowActive {
|
||||
.tableCell {
|
||||
background-color: var(--row-active-bg) !important;
|
||||
background-color: var(
|
||||
--tanstack-table-row-active-bg,
|
||||
var(--row-active-bg)
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.tableRowOdd {
|
||||
.tableCell {
|
||||
background-color: var(--tanstack-table-row-odd-bg, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
&.tableRowEven {
|
||||
.tableCell {
|
||||
background-color: var(--tanstack-table-row-even-bg, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.tableCell:first-child {
|
||||
background-color: var(
|
||||
--tanstack-first-column-bg,
|
||||
var(--tanstack-table-cell-bg, transparent)
|
||||
);
|
||||
color: var(
|
||||
--tanstack-first-column-color,
|
||||
var(--tanstack-table-cell-color, var(--l2-foreground))
|
||||
);
|
||||
}
|
||||
|
||||
&.tableRowOdd .tableCell:first-child {
|
||||
background-color: var(
|
||||
--tanstack-first-column-odd-bg,
|
||||
var(
|
||||
--tanstack-first-column-bg,
|
||||
var(--tanstack-table-row-odd-bg, transparent)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
&.tableRowEven .tableCell:first-child {
|
||||
background-color: var(
|
||||
--tanstack-first-column-even-bg,
|
||||
var(
|
||||
--tanstack-first-column-bg,
|
||||
var(--tanstack-table-row-even-bg, transparent)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.tableHeaderCell {
|
||||
padding: 0.3rem;
|
||||
padding: var(--tanstack-cell-padding-top) var(--tanstack-cell-padding-right)
|
||||
var(--tanstack-cell-padding-bottom) var(--tanstack-cell-padding-left);
|
||||
height: 36px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
@@ -78,20 +168,40 @@
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--l1-foreground);
|
||||
|
||||
// TODO: Remove this once background color (l1) is matching the actual background color of the page
|
||||
&[data-dark-mode='true'] {
|
||||
background: #0b0c0d;
|
||||
}
|
||||
|
||||
&[data-dark-mode='false'] {
|
||||
background: #fdfdfd;
|
||||
}
|
||||
color: var(--tanstack-table-header-cell-color, var(--l1-foreground));
|
||||
background: var(--tanstack-table-header-cell-bg, var(--l1-background));
|
||||
border-bottom: 1px solid var(--tanstack-table-header-border-color, transparent);
|
||||
}
|
||||
|
||||
.tableRowExpansion {
|
||||
display: table-row;
|
||||
|
||||
.tableCellExpansion {
|
||||
background-color: var(--tanstack-table-header-cell-bg, var(--l1-background));
|
||||
color: var(--tanstack-table-header-cell-color, var(--l1-foreground));
|
||||
}
|
||||
|
||||
& > td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& thead th:first-child {
|
||||
padding-left: var(
|
||||
--tanstack-expansion-first-col-padding-left,
|
||||
calc(var(--spacing-3) - 1px)
|
||||
);
|
||||
}
|
||||
|
||||
& tbody td:first-child {
|
||||
padding-left: var(
|
||||
--tanstack-expansion-first-col-padding-left,
|
||||
calc(var(--spacing-20) + var(--spacing-4))
|
||||
);
|
||||
}
|
||||
|
||||
:global(thead) {
|
||||
position: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tableCellExpansion {
|
||||
|
||||
@@ -90,6 +90,7 @@ function TanStackTableInner<TData>(
|
||||
skeletonRowCount = 10,
|
||||
enableQueryParams,
|
||||
pagination,
|
||||
paginationClassname,
|
||||
onEndReached,
|
||||
getRowKey,
|
||||
getItemKey,
|
||||
@@ -112,9 +113,17 @@ function TanStackTableInner<TData>(
|
||||
testId,
|
||||
prefixPaginationContent,
|
||||
suffixPaginationContent,
|
||||
enableAlternatingRowColors,
|
||||
disableVirtualScroll,
|
||||
}: TanStackTableProps<TData>,
|
||||
forwardedRef: React.ForwardedRef<TanStackTableHandle>,
|
||||
): JSX.Element {
|
||||
if (disableVirtualScroll && onEndReached) {
|
||||
throw new Error(
|
||||
'TanStackTable: Cannot use onEndReached with disableVirtualScroll. Infinite scroll requires virtualization.',
|
||||
);
|
||||
}
|
||||
|
||||
const virtuosoRef = useRef<TableVirtuosoHandle | null>(null);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@@ -221,6 +230,15 @@ function TanStackTableInner<TData>(
|
||||
[getRowCanExpand],
|
||||
);
|
||||
|
||||
const isExpandEnabled = Boolean(renderExpandedRow);
|
||||
useEffect(() => {
|
||||
const hasExpanded =
|
||||
typeof expanded === 'boolean' ? expanded : Object.keys(expanded).length > 0;
|
||||
if (!isExpandEnabled && hasExpanded) {
|
||||
setExpanded({});
|
||||
}
|
||||
}, [isExpandEnabled, expanded, setExpanded]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: effectiveData,
|
||||
columns: tanstackColumns,
|
||||
@@ -229,7 +247,7 @@ function TanStackTableInner<TData>(
|
||||
columnResizeMode: 'onChange',
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId,
|
||||
enableExpanding: Boolean(renderExpandedRow),
|
||||
enableExpanding: isExpandEnabled,
|
||||
getRowCanExpand: renderExpandedRow ? tableGetRowCanExpand : undefined,
|
||||
onColumnSizingChange: handleColumnSizingChange,
|
||||
onColumnVisibilityChange: noopColumnVisibility,
|
||||
@@ -333,6 +351,7 @@ function TanStackTableInner<TData>(
|
||||
hasSingleColumn,
|
||||
columnOrderKey,
|
||||
columnVisibilityKey,
|
||||
enableAlternatingRowColors,
|
||||
}),
|
||||
[
|
||||
getRowStyle,
|
||||
@@ -350,6 +369,7 @@ function TanStackTableInner<TData>(
|
||||
hasSingleColumn,
|
||||
columnOrderKey,
|
||||
columnVisibilityKey,
|
||||
enableAlternatingRowColors,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -500,7 +520,10 @@ function TanStackTableInner<TData>(
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(viewStyles.tanstackTableViewWrapper, className)}>
|
||||
<div
|
||||
className={cx(viewStyles.tanstackTableViewWrapper, className)}
|
||||
data-has-group-by={(groupBy?.length || 0) > 0}
|
||||
>
|
||||
<TanStackTableStateProvider>
|
||||
<TableLoadingSync
|
||||
isLoading={isLoading}
|
||||
@@ -508,23 +531,53 @@ function TanStackTableInner<TData>(
|
||||
/>
|
||||
<ColumnVisibilitySync visibility={effectiveVisibility} />
|
||||
<TooltipProvider>
|
||||
<TableVirtuoso<FlatItem<TData>, TableRowContext<TData>>
|
||||
className={virtuosoClassName}
|
||||
ref={virtuosoRef}
|
||||
{...restTableScrollerProps}
|
||||
data={flatItems}
|
||||
totalCount={flatItems.length}
|
||||
context={virtuosoContext}
|
||||
increaseViewportBy={INCREASE_VIEWPORT_BY}
|
||||
initialTopMostItemIndex={
|
||||
flatIndexForActiveRow >= 0 ? flatIndexForActiveRow : 0
|
||||
}
|
||||
fixedHeaderContent={tableHeader}
|
||||
style={virtuosoTableStyle}
|
||||
components={virtuosoComponents}
|
||||
endReached={onEndReached ? handleEndReached : undefined}
|
||||
data-testid={testId}
|
||||
/>
|
||||
{disableVirtualScroll ? (
|
||||
<div
|
||||
className={virtuosoClassName}
|
||||
{...restTableScrollerProps}
|
||||
data-testid={testId}
|
||||
>
|
||||
<table className={tableStyles.tanStackTable} style={virtuosoTableStyle}>
|
||||
<VirtuosoTableColGroup columns={effectiveColumns} table={table} />
|
||||
<thead>{tableHeader()}</thead>
|
||||
<tbody>
|
||||
{(isLoading && data.length === 0
|
||||
? flatItems.slice(0, skeletonRowCount)
|
||||
: flatItems
|
||||
).map((item, index) => (
|
||||
<TanStackCustomTableRow
|
||||
key={
|
||||
item.kind === 'expansion' ? `${item.row.id}-expansion` : item.row.id
|
||||
}
|
||||
item={item}
|
||||
context={virtuosoContext}
|
||||
data-index={index}
|
||||
data-item-index={index}
|
||||
data-known-size={0}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<TableVirtuoso<FlatItem<TData>, TableRowContext<TData>>
|
||||
className={virtuosoClassName}
|
||||
ref={virtuosoRef}
|
||||
{...restTableScrollerProps}
|
||||
data={flatItems}
|
||||
totalCount={flatItems.length}
|
||||
context={virtuosoContext}
|
||||
increaseViewportBy={INCREASE_VIEWPORT_BY}
|
||||
initialTopMostItemIndex={
|
||||
flatIndexForActiveRow >= 0 ? flatIndexForActiveRow : 0
|
||||
}
|
||||
fixedHeaderContent={tableHeader}
|
||||
style={virtuosoTableStyle}
|
||||
components={virtuosoComponents}
|
||||
endReached={onEndReached ? handleEndReached : undefined}
|
||||
data-testid={testId}
|
||||
/>
|
||||
)}
|
||||
{showInfiniteScrollLoader && (
|
||||
<div
|
||||
className={viewStyles.tanstackLoadingOverlay}
|
||||
@@ -534,7 +587,7 @@ function TanStackTableInner<TData>(
|
||||
</div>
|
||||
)}
|
||||
{showPagination && pagination && (
|
||||
<div className={viewStyles.paginationContainer}>
|
||||
<div className={cx(viewStyles.paginationContainer, paginationClassname)}>
|
||||
{prefixPaginationContent}
|
||||
<Pagination
|
||||
current={page}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { ColumnSizingState } from '@tanstack/react-table';
|
||||
import { Skeleton } from 'antd';
|
||||
|
||||
import { TableColumnDef } from './types';
|
||||
import { getColumnWidthStyle } from './utils';
|
||||
|
||||
import tableStyles from './TanStackTable.module.scss';
|
||||
import styles from './TanStackTableSkeleton.module.scss';
|
||||
|
||||
type TanStackTableSkeletonProps<TData> = {
|
||||
columns: TableColumnDef<TData>[];
|
||||
rowCount: number;
|
||||
isDarkMode: boolean;
|
||||
columnSizing?: ColumnSizingState;
|
||||
};
|
||||
|
||||
export function TanStackTableSkeleton<TData>({
|
||||
columns,
|
||||
rowCount,
|
||||
isDarkMode,
|
||||
columnSizing,
|
||||
}: TanStackTableSkeletonProps<TData>): JSX.Element {
|
||||
const rows = useMemo(
|
||||
() => Array.from({ length: rowCount }, (_, i) => i),
|
||||
[rowCount],
|
||||
);
|
||||
|
||||
return (
|
||||
<table className={tableStyles.tanStackTable}>
|
||||
<colgroup>
|
||||
{columns.map((column, index) => (
|
||||
<col
|
||||
key={column.id}
|
||||
style={getColumnWidthStyle(
|
||||
column,
|
||||
columnSizing?.[column.id],
|
||||
index === columns.length - 1,
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
<th
|
||||
key={column.id}
|
||||
className={tableStyles.tableHeaderCell}
|
||||
data-dark-mode={isDarkMode}
|
||||
>
|
||||
{typeof column.header === 'function' ? (
|
||||
<Skeleton.Input active size="small" className={styles.headerSkeleton} />
|
||||
) : (
|
||||
column.header
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((rowIndex) => (
|
||||
<tr key={rowIndex} className={tableStyles.tableRow}>
|
||||
{columns.map((column) => (
|
||||
<td key={column.id} className={tableStyles.tableCell}>
|
||||
<Skeleton.Input active size="small" className={styles.cellSkeleton} />
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -49,7 +49,8 @@
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--bg-slate-300) transparent;
|
||||
scrollbar-color: var(--tanstack-table-scrollbar-color, var(--bg-slate-300))
|
||||
transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
@@ -65,12 +66,12 @@
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-slate-300);
|
||||
background: var(--tanstack-table-scrollbar-color, var(--bg-slate-300));
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bg-slate-200);
|
||||
background: var(--tanstack-table-scrollbar-hover-color, var(--bg-slate-200));
|
||||
}
|
||||
|
||||
&.cellTypographySmall {
|
||||
@@ -135,18 +136,25 @@
|
||||
z-index: 3;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
background: var(--l1-background);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
background: var(--tanstack-table-loading-overlay-bg, var(--l1-background));
|
||||
box-shadow: var(
|
||||
--tanstack-table-loading-overlay-shadow,
|
||||
0 2px 8px rgba(0, 0, 0, 0.15)
|
||||
);
|
||||
}
|
||||
|
||||
:global(.lightMode) .tanstackTableVirtuosoScroll {
|
||||
scrollbar-color: var(--bg-vanilla-300) transparent;
|
||||
scrollbar-color: var(--tanstack-table-scrollbar-color, var(--bg-vanilla-300))
|
||||
transparent;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-vanilla-300);
|
||||
background: var(--tanstack-table-scrollbar-color, var(--bg-vanilla-300));
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bg-vanilla-100);
|
||||
background: var(
|
||||
--tanstack-table-scrollbar-hover-color,
|
||||
var(--bg-vanilla-100)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,6 +389,101 @@ describe('TanStackTableView Integration', () => {
|
||||
// by checking the table renders without errors
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders without errors when expanded state exists but expansion is disabled', async () => {
|
||||
// This tests that the table handles the case where URL has expanded state
|
||||
// but renderExpandedRow is undefined (expansion disabled).
|
||||
// The table's useEffect should reset expanded state automatically.
|
||||
renderTanStackTable({
|
||||
props: {
|
||||
enableQueryParams: true,
|
||||
// renderExpandedRow is undefined - expansion disabled
|
||||
},
|
||||
queryParams: { expanded: '["1"]' },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Table should render without any expanded rows
|
||||
expect(screen.queryByTestId('expanded-content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders expanded rows with unique keys in non-virtualized mode', async () => {
|
||||
// This tests that row and expansion items have unique keys to avoid
|
||||
// React's "duplicate key" warning when disableVirtualScroll is true
|
||||
renderTanStackTable({
|
||||
props: {
|
||||
disableVirtualScroll: true,
|
||||
enableQueryParams: true,
|
||||
renderExpandedRow: (row) => (
|
||||
<div data-testid={`expanded-${row.id}`}>Expanded: {row.name}</div>
|
||||
),
|
||||
getRowCanExpand: () => true,
|
||||
getRowKey: (row) => row.id,
|
||||
},
|
||||
queryParams: { expanded: '["1"]' },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Both the row and its expansion content should be rendered
|
||||
expect(screen.getByTestId('expanded-1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Expanded: Item 1')).toBeInTheDocument();
|
||||
|
||||
// Verify all 3 data rows plus 1 expansion row = 4 tr elements in tbody
|
||||
const tbody = screen.getByRole('table').querySelector('tbody');
|
||||
expect(tbody?.querySelectorAll('tr')).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disableVirtualScroll', () => {
|
||||
it('throws error when used with onEndReached', () => {
|
||||
expect(() => {
|
||||
renderTanStackTable({
|
||||
props: {
|
||||
disableVirtualScroll: true,
|
||||
onEndReached: jest.fn(),
|
||||
},
|
||||
});
|
||||
}).toThrow(
|
||||
'TanStackTable: Cannot use onEndReached with disableVirtualScroll. Infinite scroll requires virtualization.',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders all rows without virtualization', async () => {
|
||||
renderTanStackTable({
|
||||
props: {
|
||||
disableVirtualScroll: true,
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Item 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Item 3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify table structure exists
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders column headers without virtualization', async () => {
|
||||
renderTanStackTable({
|
||||
props: {
|
||||
disableVirtualScroll: true,
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('ID')).toBeInTheDocument();
|
||||
expect(screen.getByText('Name')).toBeInTheDocument();
|
||||
expect(screen.getByText('Value')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('infinite scroll', () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user