mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-22 22:20:29 +01:00
Compare commits
8 Commits
nv/integra
...
fix/genera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e204fb56f | ||
|
|
7228c029a4 | ||
|
|
63a17a66c0 | ||
|
|
b7a9fd17dc | ||
|
|
a3b4a5e7a6 | ||
|
|
9b63ab1d34 | ||
|
|
d4564df1d9 | ||
|
|
e2e050f0e0 |
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -58,7 +58,6 @@ jobs:
|
||||
- role
|
||||
- rootuser
|
||||
- serviceaccount
|
||||
- spanmapper
|
||||
- querier_json_body
|
||||
- querier_skip_resource_fingerprint
|
||||
- ttl
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
// Command dashboardmigrateintegrations runs the v1→v2 dashboard migration over
|
||||
// every bundled integration dashboard in this repo — the built-in integrations
|
||||
// under pkg/query-service/app/integrations/builtin_integrations and the cloud
|
||||
// integrations under pkg/modules/cloudintegration/.../fs/definitions — to surface
|
||||
// conversion/validation gaps and rewrite the dashboards to the v2 schema.
|
||||
//
|
||||
// It mirrors the production pipeline (module.ConvertAllV1ToV2): run the v4→v5
|
||||
// widget-query migration in place, then StorableDashboard.ConvertV1ToV2, then
|
||||
// PostableDashboardV2.Validate.
|
||||
//
|
||||
// Unlike dashboardmigraterepo (which reviews an external checkout via -out), these
|
||||
// dashboards live in this repo, so migrated output overwrites each file in place by
|
||||
// default — review the result with `git diff`. Pass -out to mirror the migrated
|
||||
// copies into a separate directory instead, leaving the repo untouched.
|
||||
//
|
||||
// Only files matching <root>/.../assets/dashboards/*.json count as dashboards;
|
||||
// integration.json, config, and other assets are skipped, as are the frozen
|
||||
// pkg/sqlmigration snapshot copies (they live outside the scanned roots).
|
||||
//
|
||||
// Throwaway tooling for the schema migration; not part of the build.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./cmd/dashboardmigrateintegrations # overwrite in place
|
||||
// go run ./cmd/dashboardmigrateintegrations -out /tmp/v2 # review copies, repo untouched
|
||||
// go run ./cmd/dashboardmigrateintegrations -only redis # just the redis dashboards
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/transition"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// integrationDashboardRoots are the two live sources of bundled integration
|
||||
// dashboards, relative to -in. The pkg/sqlmigration snapshot copies are
|
||||
// deliberately excluded — they are frozen inputs to one-time DB migrations.
|
||||
var integrationDashboardRoots = []string{
|
||||
"pkg/query-service/app/integrations/builtin_integrations",
|
||||
"pkg/modules/cloudintegration/implcloudintegration/fs/definitions",
|
||||
}
|
||||
|
||||
type outcome struct {
|
||||
relPath string
|
||||
status string // ok | skipped-v2 | convert-failed | validate-failed | read-failed | write-failed
|
||||
detail string
|
||||
}
|
||||
|
||||
func main() {
|
||||
inDir := flag.String("in", ".", "repo root to scan for integration dashboards")
|
||||
outDir := flag.String("out", "", "directory to write migrated v2 JSON (mirrors -in layout); empty = overwrite each dashboard in place")
|
||||
only := flag.String("only", "", "restrict to dashboards whose path contains this substring (e.g. redis or aws/rds); empty = all")
|
||||
flag.Parse()
|
||||
|
||||
ctx := context.Background()
|
||||
// nil duplicate-key lists == the create path / ConvertAllV1ToV2 wiring.
|
||||
migrator := transition.NewDashboardMigrateV5(slog.New(slog.NewTextHandler(os.Stderr, nil)), nil, nil)
|
||||
|
||||
var outcomes []outcome
|
||||
for _, root := range integrationDashboardRoots {
|
||||
walkRoot := filepath.Join(*inDir, root)
|
||||
err := filepath.WalkDir(walkRoot, func(path string, dirEntry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dirEntry.IsDir() || !isDashboardFile(path) {
|
||||
return nil
|
||||
}
|
||||
// rel is always computed against -in so -out mirrors the repo layout.
|
||||
rel, _ := filepath.Rel(*inDir, path)
|
||||
if *only != "" && !strings.Contains(rel, *only) {
|
||||
return nil
|
||||
}
|
||||
outcomes = append(outcomes, migrateOne(ctx, migrator, path, rel, *outDir))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "walk %s failed: %v\n", walkRoot, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
report(outcomes)
|
||||
}
|
||||
|
||||
// isDashboardFile picks out only the dashboard JSONs — those under an
|
||||
// assets/dashboards/ directory — leaving integration.json, config, and other
|
||||
// per-integration assets untouched.
|
||||
func isDashboardFile(path string) bool {
|
||||
return strings.HasSuffix(path, ".json") &&
|
||||
strings.Contains(filepath.ToSlash(path), "/assets/dashboards/")
|
||||
}
|
||||
|
||||
func migrateOne(ctx context.Context, migrator interface {
|
||||
Migrate(context.Context, map[string]any) bool
|
||||
}, path, rel, outDir string) outcome {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return outcome{rel, "read-failed", err.Error()}
|
||||
}
|
||||
var data map[string]any
|
||||
if err := json.Unmarshal(raw, &data); err != nil {
|
||||
return outcome{rel, "read-failed", err.Error()}
|
||||
}
|
||||
|
||||
storable := dashboardtypes.StorableDashboard{
|
||||
Data: dashboardtypes.StorableDashboardData(data),
|
||||
OrgID: valuer.GenerateUUID(),
|
||||
}
|
||||
storable.ID = valuer.GenerateUUID()
|
||||
|
||||
if storable.IsV2() {
|
||||
return outcome{rel, "skipped-v2", "already v2 schema"}
|
||||
}
|
||||
|
||||
// v1→v2 assumes v5-shaped widget queries; run v4→v5 in place first.
|
||||
migrator.Migrate(ctx, storable.Data)
|
||||
|
||||
v2, err := storable.ConvertV1ToV2()
|
||||
if err != nil {
|
||||
return outcome{rel, "convert-failed", err.Error()}
|
||||
}
|
||||
|
||||
out, err := marshalPostableV2(v2)
|
||||
if err != nil {
|
||||
return outcome{rel, "convert-failed", err.Error()}
|
||||
}
|
||||
|
||||
// Validate exactly as the import API does: unmarshal the JSON back. This both
|
||||
// populates common.JSONRef.Path (json:"-", set only on decode — an in-memory
|
||||
// Spec.Validate() would spuriously fail panel-ref checks) and runs the full
|
||||
// PostableDashboardV2.Validate (DisallowUnknownFields + spec validation).
|
||||
var roundTrip dashboardtypes.PostableDashboardV2
|
||||
if err := json.Unmarshal(out, &roundTrip); err != nil {
|
||||
return outcome{rel, "validate-failed", err.Error()}
|
||||
}
|
||||
|
||||
// Overwrite in place by default; mirror into -out (same layout, same name) when set.
|
||||
dst := path
|
||||
if outDir != "" {
|
||||
dst = filepath.Join(outDir, rel)
|
||||
}
|
||||
if err := writeFile(out, dst); err != nil {
|
||||
return outcome{rel, "write-failed", err.Error()}
|
||||
}
|
||||
return outcome{rel, "ok", ""}
|
||||
}
|
||||
|
||||
// marshalPostableV2 renders the PostableDashboardV2 form (schemaVersion, image,
|
||||
// tags, spec) — the shape the provisioning path decodes.
|
||||
//
|
||||
// generateName is set (and name left empty) so the committed files stay
|
||||
// deterministic — no baked-in random suffix that would churn on every re-run —
|
||||
// and each install generates a fresh per-org dashboard name from spec.display.name.
|
||||
func marshalPostableV2(v2 *dashboardtypes.DashboardV2) ([]byte, error) {
|
||||
postable := dashboardtypes.PostableDashboardV2{
|
||||
DashboardV2MetadataBase: v2.DashboardV2MetadataBase,
|
||||
GenerateName: true,
|
||||
Tags: tagtypes.NewPostableTagsFromTags(v2.Tags),
|
||||
Spec: v2.Spec,
|
||||
}
|
||||
return json.MarshalIndent(postable, "", " ")
|
||||
}
|
||||
|
||||
func writeFile(out []byte, dst string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, out, 0o644)
|
||||
}
|
||||
|
||||
func report(outcomes []outcome) {
|
||||
sort.Slice(outcomes, func(i, j int) bool { return outcomes[i].relPath < outcomes[j].relPath })
|
||||
|
||||
counts := map[string]int{}
|
||||
for _, o := range outcomes {
|
||||
counts[o.status]++
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== %d dashboards ===\n", len(outcomes))
|
||||
for _, status := range []string{"ok", "skipped-v2", "convert-failed", "validate-failed", "read-failed", "write-failed"} {
|
||||
if counts[status] > 0 {
|
||||
fmt.Printf(" %-16s %d\n", status, counts[status])
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== failures ===\n")
|
||||
any := false
|
||||
for _, o := range outcomes {
|
||||
if o.status == "ok" || o.status == "skipped-v2" {
|
||||
continue
|
||||
}
|
||||
any = true
|
||||
fmt.Printf("\n[%s] %s\n %s\n", o.status, o.relPath, o.detail)
|
||||
}
|
||||
if !any {
|
||||
fmt.Println(" none")
|
||||
}
|
||||
}
|
||||
@@ -1495,7 +1495,6 @@ components:
|
||||
- cassandradb
|
||||
- redis
|
||||
- cloudsql_postgres
|
||||
- memorystore_redis
|
||||
type: string
|
||||
CloudintegrationtypesServiceMetadata:
|
||||
properties:
|
||||
@@ -8085,19 +8084,6 @@ components:
|
||||
required:
|
||||
- items
|
||||
type: object
|
||||
SpantypesGettableSpanMapperTest:
|
||||
properties:
|
||||
collectorLogs:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
spans:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperTestSpan'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
SpantypesGettableSpanMappers:
|
||||
properties:
|
||||
items:
|
||||
@@ -8194,39 +8180,6 @@ components:
|
||||
- name
|
||||
- condition
|
||||
type: object
|
||||
SpantypesPostableSpanMapperTest:
|
||||
properties:
|
||||
groups:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesPostableSpanMapperTestGroup'
|
||||
nullable: true
|
||||
type: array
|
||||
spans:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperTestSpan'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- spans
|
||||
- groups
|
||||
type: object
|
||||
SpantypesPostableSpanMapperTestGroup:
|
||||
properties:
|
||||
condition:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperGroupCondition'
|
||||
enabled:
|
||||
type: boolean
|
||||
mappers:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesPostableSpanMapper'
|
||||
nullable: true
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- condition
|
||||
type: object
|
||||
SpantypesPostableTraceAggregations:
|
||||
properties:
|
||||
aggregations:
|
||||
@@ -8388,17 +8341,6 @@ components:
|
||||
- operation
|
||||
- priority
|
||||
type: object
|
||||
SpantypesSpanMapperTestSpan:
|
||||
properties:
|
||||
attributes:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
resource:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
type: object
|
||||
SpantypesUpdatableSpanMapper:
|
||||
properties:
|
||||
config:
|
||||
@@ -14132,69 +14074,6 @@ paths:
|
||||
summary: Update a span mapper
|
||||
tags:
|
||||
- spanmapper
|
||||
/api/v1/span_mapper_groups/test:
|
||||
post:
|
||||
deprecated: false
|
||||
description: Tests how span mappers would transform sample spans
|
||||
operationId: TestSpanMappers
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpantypesPostableSpanMapperTest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SpantypesGettableSpanMapperTest'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Test span mappers against sample spans
|
||||
tags:
|
||||
- spanmapper
|
||||
/api/v1/stats:
|
||||
get:
|
||||
deprecated: false
|
||||
|
||||
@@ -551,12 +551,12 @@ func (module *module) provisionDashboards(ctx context.Context, orgID valuer.UUID
|
||||
continue
|
||||
}
|
||||
|
||||
createdDashboard, err := module.dashboardModule.CreateV2(ctx, orgID, createdBy, creator, dashboardtypes.SourceIntegration, dashboard.Definition)
|
||||
createdDashboard, err := module.dashboardModule.Create(ctx, orgID, createdBy, creator, dashboardtypes.SourceIntegration, dashboardtypes.PostableDashboard(dashboard.Definition))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
integrationDashboard := cloudintegrationtypes.NewStorableIntegrationDashboard(createdDashboard.ID.StringValue(), cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
|
||||
integrationDashboard := cloudintegrationtypes.NewStorableIntegrationDashboard(createdDashboard.ID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
|
||||
if err := module.store.CreateIntegrationDashboard(ctx, integrationDashboard); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"lodash-es": "^4.17.21",
|
||||
"motion": "12.4.13",
|
||||
"nuqs": "2.8.8",
|
||||
"overlayscrollbars": "^2.16.0",
|
||||
"overlayscrollbars": "^2.8.1",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"papaparse": "5.4.1",
|
||||
"posthog-js": "1.298.0",
|
||||
@@ -221,18 +221,5 @@
|
||||
"*.(scss|css)": [
|
||||
"stylelint"
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"@babel/core@<=7.29.0": ">=7.29.6 <8",
|
||||
"@istanbuljs/load-nyc-config>js-yaml": ">=4.2.0 <5",
|
||||
"cookie@<0.7.0": ">=0.7.1 <1",
|
||||
"dompurify@<=3.4.10": ">=3.4.11 <4",
|
||||
"esbuild@>=0.27.3 <0.28.1": ">=0.28.1 <0.29.0",
|
||||
"js-cookie@<=3.0.5": ">=3.0.7 <4",
|
||||
"js-yaml@>=4.0.0 <=4.1.1": ">=4.2.0 <5",
|
||||
"prismjs@<1.30.0": ">=1.30.0 <2",
|
||||
"react-router@>=6.7.0 <6.30.4": ">=6.30.4 <7",
|
||||
"tmp@<0.2.6": ">=0.2.6 <0.3.0",
|
||||
"yaml@>=1.0.0 <1.10.3": ">=1.10.3 <2"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
frontend/pnpm-lock.yaml
generated
16
frontend/pnpm-lock.yaml
generated
@@ -187,11 +187,11 @@ importers:
|
||||
specifier: 2.8.8
|
||||
version: 2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)
|
||||
overlayscrollbars:
|
||||
specifier: ^2.16.0
|
||||
version: 2.16.0
|
||||
specifier: ^2.8.1
|
||||
version: 2.9.2
|
||||
overlayscrollbars-react:
|
||||
specifier: ^0.5.6
|
||||
version: 0.5.6(overlayscrollbars@2.16.0)(react@18.2.0)
|
||||
version: 0.5.6(overlayscrollbars@2.9.2)(react@18.2.0)
|
||||
papaparse:
|
||||
specifier: 5.4.1
|
||||
version: 5.4.1
|
||||
@@ -6941,8 +6941,8 @@ packages:
|
||||
overlayscrollbars: ^2.0.0
|
||||
react: '>=16.8.0'
|
||||
|
||||
overlayscrollbars@2.16.0:
|
||||
resolution: {integrity: sha512-N03oje/q7j93D0aLZtoCdsDSYLmhheSsv8H7oSLE7HhdV9P/bmCURtLV/KbPye7P/bpfyt/obSfDpGUYoJ0OWg==}
|
||||
overlayscrollbars@2.9.2:
|
||||
resolution: {integrity: sha512-iDT84r39i7oWP72diZN2mbJUsn/taCq568aQaIrc84S87PunBT7qtsVltAF2esk7ORTRjQDnfjVYoqqTzgs8QA==}
|
||||
|
||||
oxfmt@0.54.0:
|
||||
resolution: {integrity: sha512-DjnMwn7smSLF+Mc2+pRItnuPftm/dkUFpY/d4+33y9TfKrsHZo8GLhmUg9BrOIUEy94Rlom1Q11N6vuhE+e0oQ==}
|
||||
@@ -16456,12 +16456,12 @@ snapshots:
|
||||
|
||||
outvariant@1.4.0: {}
|
||||
|
||||
overlayscrollbars-react@0.5.6(overlayscrollbars@2.16.0)(react@18.2.0):
|
||||
overlayscrollbars-react@0.5.6(overlayscrollbars@2.9.2)(react@18.2.0):
|
||||
dependencies:
|
||||
overlayscrollbars: 2.16.0
|
||||
overlayscrollbars: 2.9.2
|
||||
react: 18.2.0
|
||||
|
||||
overlayscrollbars@2.16.0: {}
|
||||
overlayscrollbars@2.9.2: {}
|
||||
|
||||
oxfmt@0.54.0:
|
||||
dependencies:
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
|
||||
export function mockFieldsAPIsWithEmptyResponse(): void {
|
||||
server.use(
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: { keys: {}, complete: true } }),
|
||||
),
|
||||
),
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/values`, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: { values: {}, complete: true } }),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,29 @@ import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
// The wire shape these handlers can actually rely on. The generated
|
||||
// RenderErrorResponseDTO marks code/message/url/errors as required, but the
|
||||
// server omits any of them even on valid errors (e.g. a 400 with just a
|
||||
// message), so a present `error` object is all the guard can guarantee.
|
||||
type ErrorEnvelope = {
|
||||
error: {
|
||||
code?: string;
|
||||
message?: string;
|
||||
url?: string;
|
||||
errors?: { message?: string }[];
|
||||
};
|
||||
};
|
||||
|
||||
function isErrorEnvelope(data: unknown): data is ErrorEnvelope {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'error' in data &&
|
||||
typeof (data as ErrorEnvelope).error === 'object' &&
|
||||
(data as ErrorEnvelope).error !== null
|
||||
);
|
||||
}
|
||||
|
||||
// @deprecated Use convertToApiError instead
|
||||
export function ErrorResponseHandlerForGeneratedAPIs(
|
||||
error: AxiosError<RenderErrorResponseDTO>,
|
||||
@@ -10,15 +33,29 @@ export function ErrorResponseHandlerForGeneratedAPIs(
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
if (response) {
|
||||
// The body isn't guaranteed to be an error envelope — e.g. a gateway 5xx
|
||||
// with an HTML/empty body during a deploy. Verify the shape before reading
|
||||
// it; otherwise synthesize a consistent error from the status.
|
||||
const data: unknown = response.data;
|
||||
if (isErrorEnvelope(data)) {
|
||||
const { code, message, url, errors } = data.error;
|
||||
throw new APIError({
|
||||
httpStatusCode: response.status || 500,
|
||||
error: {
|
||||
code: code ?? '',
|
||||
message: message ?? '',
|
||||
url: url ?? '',
|
||||
errors: (errors ?? []).map((e) => ({ message: e.message ?? '' })),
|
||||
},
|
||||
});
|
||||
}
|
||||
throw new APIError({
|
||||
httpStatusCode: response.status || 500,
|
||||
error: {
|
||||
code: response.data.error.code,
|
||||
message: response.data.error.message,
|
||||
url: response.data.error.url ?? '',
|
||||
errors: (response.data.error.errors ?? []).map((e) => ({
|
||||
message: e.message ?? '',
|
||||
})),
|
||||
code: 'UPSTREAM_UNAVAILABLE',
|
||||
message: error.message || 'Something went wrong',
|
||||
url: '',
|
||||
errors: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -62,9 +99,7 @@ export function convertToApiError(
|
||||
return new APIError({
|
||||
httpStatusCode: response?.status || error.status || 500,
|
||||
error: {
|
||||
code:
|
||||
errorData?.code ||
|
||||
String(response?.status || error.code || 'unknown_error'),
|
||||
code: errorData?.code || 'UPSTREAM_UNAVAILABLE',
|
||||
message:
|
||||
errorData?.message ||
|
||||
response?.statusText ||
|
||||
|
||||
@@ -2,19 +2,38 @@ import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
function isErrorV2Resp(data: unknown): data is ErrorV2Resp {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'error' in data &&
|
||||
typeof (data as ErrorV2Resp).error?.code === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
// reference - https://axios-http.com/docs/handling_errors
|
||||
export function ErrorResponseHandlerV2(error: AxiosError<ErrorV2Resp>): never {
|
||||
const { response, request } = error;
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
if (response) {
|
||||
// response.data isn't guaranteed to be a V2 envelope (e.g. a gateway 5xx
|
||||
// with an HTML/empty body during a deploy), so verify the shape first.
|
||||
const data: unknown = response.data;
|
||||
if (isErrorV2Resp(data)) {
|
||||
const { code, message, url, errors } = data.error;
|
||||
throw new APIError({
|
||||
httpStatusCode: response.status || 500,
|
||||
error: { code, message, url, errors },
|
||||
});
|
||||
}
|
||||
throw new APIError({
|
||||
httpStatusCode: response.status || 500,
|
||||
error: {
|
||||
code: response.data.error.code,
|
||||
message: response.data.error.message,
|
||||
url: response.data.error.url,
|
||||
errors: response.data.error.errors,
|
||||
code: 'UPSTREAM_UNAVAILABLE',
|
||||
message: error.message || 'Something went wrong',
|
||||
url: '',
|
||||
errors: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
126
frontend/src/api/__tests__/ErrorResponseHandlerV2.test.ts
Normal file
126
frontend/src/api/__tests__/ErrorResponseHandlerV2.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
function asAxiosError(partial: Partial<AxiosError>): AxiosError<ErrorV2Resp> {
|
||||
return partial as AxiosError<ErrorV2Resp>;
|
||||
}
|
||||
|
||||
// ErrorResponseHandlerV2 always throws — capture the APIError so assertions stay
|
||||
// unconditional.
|
||||
function runHandler(error: AxiosError<ErrorV2Resp>): APIError {
|
||||
try {
|
||||
ErrorResponseHandlerV2(error);
|
||||
} catch (thrown) {
|
||||
return thrown as APIError;
|
||||
}
|
||||
throw new Error('expected ErrorResponseHandlerV2 to throw');
|
||||
}
|
||||
|
||||
type ExpectedError = {
|
||||
httpStatusCode: number;
|
||||
code: string;
|
||||
message: string;
|
||||
errors: { message: string }[];
|
||||
};
|
||||
|
||||
// One row per response shape the handler must normalize. New shapes (with
|
||||
// different bodies) can be added here without a new test block.
|
||||
const cases: {
|
||||
name: string;
|
||||
error: AxiosError<ErrorV2Resp>;
|
||||
expected: ExpectedError;
|
||||
}[] = [
|
||||
{
|
||||
name: 'well-formed V2 error envelope',
|
||||
error: asAxiosError({
|
||||
message: 'Request failed with status code 400',
|
||||
response: {
|
||||
status: 400,
|
||||
data: {
|
||||
error: {
|
||||
code: 'bad_request',
|
||||
message: 'Invalid dashboard payload',
|
||||
url: 'https://signoz.io/docs',
|
||||
errors: [{ message: 'name is required' }, { message: 'name too long' }],
|
||||
},
|
||||
},
|
||||
} as AxiosError['response'],
|
||||
}),
|
||||
expected: {
|
||||
httpStatusCode: 400,
|
||||
code: 'bad_request',
|
||||
message: 'Invalid dashboard payload',
|
||||
errors: [{ message: 'name is required' }, { message: 'name too long' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Regression: during a deployment the gateway returns a 5xx with a
|
||||
// non-envelope body. Reading response.data.error.code used to throw a
|
||||
// TypeError from inside the handler itself. See engineering-pod#5760.
|
||||
name: '5xx with a non-envelope HTML body',
|
||||
error: asAxiosError({
|
||||
message: 'Request failed with status code 503',
|
||||
response: {
|
||||
status: 503,
|
||||
data: '<html><body>503 Service Temporarily Unavailable</body></html>',
|
||||
} as AxiosError['response'],
|
||||
}),
|
||||
expected: {
|
||||
httpStatusCode: 503,
|
||||
code: 'UPSTREAM_UNAVAILABLE',
|
||||
message: 'Request failed with status code 503',
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '5xx with an empty body',
|
||||
error: asAxiosError({
|
||||
message: 'Request failed with status code 502',
|
||||
response: {
|
||||
status: 502,
|
||||
data: undefined,
|
||||
} as AxiosError['response'],
|
||||
}),
|
||||
expected: {
|
||||
httpStatusCode: 502,
|
||||
code: 'UPSTREAM_UNAVAILABLE',
|
||||
message: 'Request failed with status code 502',
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'no response received (network error)',
|
||||
error: asAxiosError({
|
||||
message: 'Network Error',
|
||||
code: 'ERR_NETWORK',
|
||||
name: 'AxiosError',
|
||||
request: {},
|
||||
}),
|
||||
expected: {
|
||||
httpStatusCode: 500,
|
||||
code: 'ERR_NETWORK',
|
||||
message: 'Network Error',
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('ErrorResponseHandlerV2', () => {
|
||||
it.each(cases)(
|
||||
'normalizes $name into a consistent APIError',
|
||||
({ error, expected }) => {
|
||||
const apiError = runHandler(error);
|
||||
|
||||
expect(apiError).toBeInstanceOf(APIError);
|
||||
expect(apiError.getHttpStatusCode()).toBe(expected.httpStatusCode);
|
||||
expect(apiError.getErrorCode()).toBe(expected.code);
|
||||
expect(apiError.getErrorMessage()).toBe(expected.message);
|
||||
// The sub-error messages feed several parts of the UI, so assert them.
|
||||
expect(apiError.getErrorDetails().error.errors).toStrictEqual(
|
||||
expected.errors,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -2814,7 +2814,6 @@ export enum CloudintegrationtypesServiceIDDTO {
|
||||
cassandradb = 'cassandradb',
|
||||
redis = 'redis',
|
||||
cloudsql_postgres = 'cloudsql_postgres',
|
||||
memorystore_redis = 'memorystore_redis',
|
||||
}
|
||||
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
|
||||
/**
|
||||
@@ -9271,48 +9270,6 @@ export interface SpantypesGettableSpanMapperGroupsDTO {
|
||||
items: SpantypesSpanMapperGroupDTO[];
|
||||
}
|
||||
|
||||
export type SpantypesSpanMapperTestSpanDTOAttributesAnyOf = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesSpanMapperTestSpanDTOAttributes =
|
||||
SpantypesSpanMapperTestSpanDTOAttributesAnyOf | null;
|
||||
|
||||
export type SpantypesSpanMapperTestSpanDTOResourceAnyOf = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesSpanMapperTestSpanDTOResource =
|
||||
SpantypesSpanMapperTestSpanDTOResourceAnyOf | null;
|
||||
|
||||
export interface SpantypesSpanMapperTestSpanDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
attributes?: SpantypesSpanMapperTestSpanDTOAttributes;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
resource?: SpantypesSpanMapperTestSpanDTOResource;
|
||||
}
|
||||
|
||||
export interface SpantypesGettableSpanMapperTestDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
collectorLogs?: string[] | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
spans?: SpantypesSpanMapperTestSpanDTO[] | null;
|
||||
}
|
||||
|
||||
export enum SpantypesSpanMapperOperationDTO {
|
||||
move = 'move',
|
||||
copy = 'copy',
|
||||
@@ -9654,33 +9611,6 @@ export interface SpantypesPostableSpanMapperGroupDTO {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SpantypesPostableSpanMapperTestGroupDTO {
|
||||
condition: SpantypesSpanMapperGroupConditionDTO | null;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
mappers?: SpantypesPostableSpanMapperDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SpantypesPostableSpanMapperTestDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
groups: SpantypesPostableSpanMapperTestGroupDTO[] | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
spans: SpantypesSpanMapperTestSpanDTO[] | null;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanAggregationDTO {
|
||||
aggregation: SpantypesSpanAggregationTypeDTO;
|
||||
field: TelemetrytypesTelemetryFieldKeyDTO;
|
||||
@@ -11031,14 +10961,6 @@ export type UpdateSpanMapperPathParameters = {
|
||||
groupId: string;
|
||||
mapperId: string;
|
||||
};
|
||||
export type TestSpanMappers200 = {
|
||||
data: SpantypesGettableSpanMapperTestDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetStats200Data = { [key: string]: unknown };
|
||||
|
||||
export type GetStats200 = {
|
||||
|
||||
@@ -30,10 +30,8 @@ import type {
|
||||
RenderErrorResponseDTO,
|
||||
SpantypesPostableSpanMapperDTO,
|
||||
SpantypesPostableSpanMapperGroupDTO,
|
||||
SpantypesPostableSpanMapperTestDTO,
|
||||
SpantypesUpdatableSpanMapperDTO,
|
||||
SpantypesUpdatableSpanMapperGroupDTO,
|
||||
TestSpanMappers200,
|
||||
UpdateSpanMapperGroupPathParameters,
|
||||
UpdateSpanMapperPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
@@ -782,86 +780,3 @@ export const useUpdateSpanMapper = <
|
||||
> => {
|
||||
return useMutation(getUpdateSpanMapperMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Tests how span mappers would transform sample spans
|
||||
* @summary Test span mappers against sample spans
|
||||
*/
|
||||
export const testSpanMappers = (
|
||||
spantypesPostableSpanMapperTestDTO?: BodyType<SpantypesPostableSpanMapperTestDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<TestSpanMappers200>({
|
||||
url: `/api/v1/span_mapper_groups/test`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: spantypesPostableSpanMapperTestDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getTestSpanMappersMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['testSpanMappers'];
|
||||
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 testSpanMappers>>,
|
||||
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return testSpanMappers(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type TestSpanMappersMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>
|
||||
>;
|
||||
export type TestSpanMappersMutationBody =
|
||||
| BodyType<SpantypesPostableSpanMapperTestDTO>
|
||||
| undefined;
|
||||
export type TestSpanMappersMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Test span mappers against sample spans
|
||||
*/
|
||||
export const useTestSpanMappers = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getTestSpanMappersMutationOptions(options));
|
||||
};
|
||||
|
||||
@@ -52,8 +52,6 @@ import type { SignalType } from 'types/api/v5/queryRange';
|
||||
import { queryExamples, SUGGESTIONS_SECTION } from './constants';
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
dedupeOptionsByLabel,
|
||||
getFieldContextPrefix,
|
||||
getRecentOptions,
|
||||
renderRecentDeleteButton,
|
||||
} from './utils';
|
||||
@@ -224,12 +222,6 @@ function QuerySearch({
|
||||
QueryKeyDataSuggestionsProps[] | null
|
||||
>(null);
|
||||
|
||||
// dedupe keySuggestions by label/name
|
||||
const dedupedKeySuggestions = useMemo(
|
||||
() => dedupeOptionsByLabel(keySuggestions || []),
|
||||
[keySuggestions],
|
||||
);
|
||||
|
||||
const [showExamples] = useState(false);
|
||||
|
||||
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
|
||||
@@ -254,11 +246,9 @@ function QuerySearch({
|
||||
[key: string]: QueryKeyDataSuggestionsProps[];
|
||||
}): any[] =>
|
||||
Object.values(keys).flatMap((items: QueryKeyDataSuggestionsProps[]) =>
|
||||
items.map(({ name, fieldDataType, fieldContext }) => ({
|
||||
items.map(({ name, fieldDataType }) => ({
|
||||
label: name,
|
||||
type: fieldDataType === 'string' ? 'keyword' : fieldDataType,
|
||||
fieldContext,
|
||||
fieldDataType,
|
||||
info: '',
|
||||
details: '',
|
||||
})),
|
||||
@@ -317,17 +307,13 @@ function QuerySearch({
|
||||
if (response.data.data) {
|
||||
const { keys } = response.data.data;
|
||||
const options = generateOptions(keys);
|
||||
// Deduplicate by full variant identity (name + context + data type), NOT by
|
||||
// label. deduping by label removes varient which is not expected. If we need
|
||||
// to dedupe by label use dedupedKeySuggestions not dedupe the source itself
|
||||
const variantId = (opt: QueryKeyDataSuggestionsProps): string =>
|
||||
`${opt.label}|${opt.fieldContext ?? ''}|${opt.fieldDataType ?? ''}`;
|
||||
// Use a Map to deduplicate by label and preserve order: new options take precedence
|
||||
const merged = new Map<string, QueryKeyDataSuggestionsProps>();
|
||||
options.forEach((opt) => merged.set(variantId(opt), opt));
|
||||
options.forEach((opt) => merged.set(opt.label, opt));
|
||||
if (searchText && lastKeyRef.current !== searchText) {
|
||||
(keySuggestions || []).forEach((opt) => {
|
||||
if (!merged.has(variantId(opt))) {
|
||||
merged.set(variantId(opt), opt);
|
||||
if (!merged.has(opt.label)) {
|
||||
merged.set(opt.label, opt);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -933,55 +919,8 @@ function QuerySearch({
|
||||
|
||||
if (queryContext.isInKey) {
|
||||
const searchText = word?.text.toLowerCase().trim() ?? '';
|
||||
const fieldContextMatch = getFieldContextPrefix(searchText);
|
||||
|
||||
if (fieldContextMatch) {
|
||||
const { context: fieldContext, remainder } = fieldContextMatch;
|
||||
|
||||
// Fetch the context's page when the prefix is typed exactly eg.("attribute.")
|
||||
if (remainder === '' && lastFetchedKeyRef.current !== searchText) {
|
||||
debouncedFetchKeySuggestions(searchText);
|
||||
}
|
||||
|
||||
//suggestions that actually do start with <fieldContext>.
|
||||
const nameMatches = (keySuggestions || [])
|
||||
.filter((option) => option.label.toLowerCase().includes(searchText))
|
||||
.map((option) => ({ ...option, boost: 100 }));
|
||||
|
||||
//suggestions which do not start with the prefix but qualifies for suggestion
|
||||
const contextQualified = (keySuggestions || [])
|
||||
.filter(
|
||||
(option) =>
|
||||
option.fieldContext === fieldContext &&
|
||||
option.label.toLowerCase().includes(remainder),
|
||||
)
|
||||
.map((option) => ({
|
||||
...option,
|
||||
label: `${fieldContext}.${option.label}`,
|
||||
boost: 0,
|
||||
}));
|
||||
|
||||
const contextOptions = dedupeOptionsByLabel([
|
||||
...nameMatches,
|
||||
...contextQualified,
|
||||
]);
|
||||
|
||||
// If contextOptions is empty fetch again.
|
||||
if (
|
||||
contextOptions.length === 0 &&
|
||||
lastFetchedKeyRef.current !== searchText
|
||||
) {
|
||||
debouncedFetchKeySuggestions(searchText);
|
||||
}
|
||||
|
||||
return {
|
||||
from: word?.from ?? 0,
|
||||
to: word?.to ?? cursorPos.ch,
|
||||
options: addSpaceToOptions(contextOptions),
|
||||
};
|
||||
}
|
||||
|
||||
options = dedupedKeySuggestions.filter((option) =>
|
||||
options = (keySuggestions || []).filter((option) =>
|
||||
option.label.toLowerCase().includes(searchText),
|
||||
);
|
||||
|
||||
@@ -1030,26 +969,9 @@ function QuerySearch({
|
||||
|
||||
// If we have a key context, add that info to the operator suggestions
|
||||
if (keyName) {
|
||||
const keyContextMatch = getFieldContextPrefix(keyName);
|
||||
// key-suggestion can contain multiple variants of a single key
|
||||
// In variants we capture ones that match the label to typed keyName exactly or,
|
||||
// if it has a prefix fieldContext remove it and then match.
|
||||
const variants = (keySuggestions || []).filter(
|
||||
(k) =>
|
||||
k.label === keyName ||
|
||||
(keyContextMatch !== null &&
|
||||
k.fieldContext === keyContextMatch.context &&
|
||||
k.label === keyContextMatch.remainder),
|
||||
);
|
||||
const variantTypes = new Set(
|
||||
variants
|
||||
.map((k) =>
|
||||
k.type === 'keyword' ? QUERY_BUILDER_KEY_TYPES.STRING : k.type,
|
||||
)
|
||||
.filter(Boolean),
|
||||
);
|
||||
//if there are multi-variant, show all suggestions else just the one
|
||||
const keyType = variantTypes.size === 1 ? [...variantTypes][0] : '';
|
||||
// Find the key details from suggestions
|
||||
const keyDetails = (keySuggestions || []).find((k) => k.label === keyName);
|
||||
const keyType = keyDetails?.type || '';
|
||||
|
||||
// Filter operators based on key type
|
||||
if (keyType) {
|
||||
@@ -1292,7 +1214,7 @@ function QuerySearch({
|
||||
if (curChar === '(') {
|
||||
// In expression context, suggest keys, functions, or nested parentheses
|
||||
options = [
|
||||
...dedupedKeySuggestions,
|
||||
...(keySuggestions || []),
|
||||
{ label: '(', type: 'parenthesis', info: 'Open nested group' },
|
||||
{ label: 'NOT', type: 'operator', info: 'Negate expression' },
|
||||
...options.filter((opt) => opt.type === 'function'),
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
dedupeOptionsByLabel,
|
||||
getFieldContextPrefix,
|
||||
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"',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFieldContextPrefix', () => {
|
||||
it('matches a complete context prefix with a remainder', () => {
|
||||
expect(getFieldContextPrefix('attribute.status')).toStrictEqual({
|
||||
context: 'attribute',
|
||||
remainder: 'status',
|
||||
});
|
||||
});
|
||||
|
||||
it('matches a bare context prefix with empty remainder', () => {
|
||||
expect(getFieldContextPrefix('resource.')).toStrictEqual({
|
||||
context: 'resource',
|
||||
remainder: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('matches every backend field context', () => {
|
||||
['attribute', 'resource', 'span', 'body', 'log', 'metric'].forEach((ctx) => {
|
||||
expect(getFieldContextPrefix(`${ctx}.x`)).toStrictEqual({
|
||||
context: ctx,
|
||||
remainder: 'x',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not match a partial context name', () => {
|
||||
expect(getFieldContextPrefix('attr')).toBeNull();
|
||||
expect(getFieldContextPrefix('attribute')).toBeNull();
|
||||
});
|
||||
|
||||
it('does not match a non-context key with dots', () => {
|
||||
expect(getFieldContextPrefix('status.code')).toBeNull();
|
||||
});
|
||||
|
||||
it('matches context case-insensitively but keeps remainder casing', () => {
|
||||
expect(getFieldContextPrefix('Attribute.Status')).toStrictEqual({
|
||||
context: 'attribute',
|
||||
remainder: 'Status',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dedupeOptionsByLabel', () => {
|
||||
it('keeps the first occurrence per label, preserving order', () => {
|
||||
expect(
|
||||
dedupeOptionsByLabel([
|
||||
{ label: 'status.code', type: 'keyword' },
|
||||
{ label: 'status.code', type: 'number' },
|
||||
{ label: 'duration', type: 'number' },
|
||||
]),
|
||||
).toStrictEqual([
|
||||
{ label: 'status.code', type: 'keyword' },
|
||||
{ label: 'duration', type: 'number' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns an empty array for empty input', () => {
|
||||
expect(dedupeOptionsByLabel([])).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -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"',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,6 @@
|
||||
export const RECENTS_SECTION = { name: 'Recent searches', rank: 1 } as const;
|
||||
export const SUGGESTIONS_SECTION = { name: 'Suggestions', rank: 2 } as const;
|
||||
|
||||
// TODO: move to using TelemetrytypesFieldContextDTO when we migrate getKeySuggestions API (Part of https://github.com/SigNoz/engineering-pod/issues/5289)
|
||||
export const FIELD_CONTEXTS = [
|
||||
'attribute',
|
||||
'resource',
|
||||
'span',
|
||||
'body',
|
||||
'log',
|
||||
'metric',
|
||||
] as const;
|
||||
|
||||
// Custom CodeMirror Completion.type for recent-query entries. Used to discriminate
|
||||
// recents from regular autocomplete completions in renderers and event handlers.
|
||||
export const RECENT_COMPLETION_TYPE = 'recent';
|
||||
|
||||
@@ -9,45 +9,11 @@ import type { SignalType } from 'types/api/v5/queryRange';
|
||||
import 'utils/timeUtils';
|
||||
|
||||
import {
|
||||
FIELD_CONTEXTS,
|
||||
RECENT_COMPLETION_TYPE,
|
||||
RECENTS_DISPLAY_CAP,
|
||||
RECENTS_SECTION,
|
||||
} from './constants';
|
||||
|
||||
export interface FieldContextPrefixMatch {
|
||||
context: string;
|
||||
remainder: string;
|
||||
}
|
||||
|
||||
// This function checks if the text(query key) starts with a fieldContext
|
||||
// This util strictly checks that and returns context and remainder back.
|
||||
// This helps differentiate if the typed key was prefixed with a context or
|
||||
// was it an actual queryKey like (attribute.abc)
|
||||
export function getFieldContextPrefix(
|
||||
text: string,
|
||||
): FieldContextPrefixMatch | null {
|
||||
const lower = text.toLowerCase();
|
||||
const context = FIELD_CONTEXTS.find((ctx) => lower.startsWith(`${ctx}.`));
|
||||
return context ? { context, remainder: text.slice(context.length + 1) } : null;
|
||||
}
|
||||
|
||||
// Keeps the first occurrence per label, preserving order. Key suggestions hold
|
||||
// one entry per (name, fieldContext, fieldDataType) variant; This means query builder
|
||||
// could show multiple labels and this avoids that.
|
||||
export function dedupeOptionsByLabel<T extends { label: string }>(
|
||||
options: T[],
|
||||
): T[] {
|
||||
const seen = new Set<string>();
|
||||
return options.filter((option) => {
|
||||
if (seen.has(option.label)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(option.label);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function combineInitialAndUserExpression(
|
||||
initial: string,
|
||||
user: string,
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import QuerySearch from '../QuerySearch/QuerySearch';
|
||||
import { mockCodeMirrorDomApis } from './codemirrorDomMocks';
|
||||
|
||||
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
|
||||
const CM_OPTION_SELECTOR = '.cm-tooltip-autocomplete .cm-completionLabel';
|
||||
|
||||
beforeAll(() => {
|
||||
mockCodeMirrorDomApis();
|
||||
});
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { dashboardData: undefined } => ({
|
||||
dashboardData: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
|
||||
const handleRunQuery = jest.fn();
|
||||
return {
|
||||
__esModule: true,
|
||||
useQueryBuilder: (): { handleRunQuery: () => void } => ({ handleRunQuery }),
|
||||
handleRunQuery,
|
||||
};
|
||||
});
|
||||
|
||||
// Keys fixture: status.code exists as 3 variants (attribute string/number +
|
||||
// resource string); attribute.a.b.c is a key literally named with the prefix.
|
||||
const KEYS_FIXTURE = {
|
||||
'status.code': [
|
||||
{
|
||||
name: 'status.code',
|
||||
fieldContext: 'attribute',
|
||||
fieldDataType: 'string',
|
||||
signal: 'logs',
|
||||
},
|
||||
{
|
||||
name: 'status.code',
|
||||
fieldContext: 'attribute',
|
||||
fieldDataType: 'number',
|
||||
signal: 'logs',
|
||||
},
|
||||
{
|
||||
name: 'status.code',
|
||||
fieldContext: 'resource',
|
||||
fieldDataType: 'string',
|
||||
signal: 'logs',
|
||||
},
|
||||
],
|
||||
'attribute.a.b.c': [
|
||||
{
|
||||
name: 'attribute.a.b.c',
|
||||
fieldContext: 'attribute',
|
||||
fieldDataType: 'string',
|
||||
signal: 'logs',
|
||||
},
|
||||
],
|
||||
'duration.nano': [
|
||||
{
|
||||
name: 'duration.nano',
|
||||
fieldContext: 'span',
|
||||
fieldDataType: 'number',
|
||||
signal: 'logs',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const fetchedSearchTexts: string[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
fetchedSearchTexts.length = 0;
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/fields/keys', (req, res, ctx) => {
|
||||
fetchedSearchTexts.push(req.url.searchParams.get('searchText') ?? '');
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: { complete: true, keys: KEYS_FIXTURE },
|
||||
}),
|
||||
);
|
||||
}),
|
||||
rest.get('http://localhost/api/v1/fields/values', (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: { values: { stringValues: [], numberValues: [] } },
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
async function renderAndType(text: string): Promise<HTMLElement> {
|
||||
render(
|
||||
<QuerySearch
|
||||
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
|
||||
queryData={initialQueriesMap.logs.builder.queryData[0]}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector(CM_EDITOR_SELECTOR)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
|
||||
await userEvent.click(editor);
|
||||
await userEvent.type(editor, text);
|
||||
return editor;
|
||||
}
|
||||
|
||||
// Types a key, waits for its suggestion to render (proving the debounced key
|
||||
// fetch resolved into state), then types a space to enter operator context.
|
||||
async function typeKeyThenEnterOperatorContext(
|
||||
key: string,
|
||||
expectedKeyLabel: string,
|
||||
): Promise<void> {
|
||||
const editor = await renderAndType(key);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(visibleOptionLabels()).toContain(expectedKeyLabel);
|
||||
});
|
||||
|
||||
// skipClick: a click would reset the caret to position 0 under the mocked
|
||||
// DOM rects; we need the space appended at the end of the typed key.
|
||||
await userEvent.type(editor, ' ', { skipClick: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(visibleOptionLabels()).toContain('=');
|
||||
});
|
||||
}
|
||||
|
||||
function visibleOptionLabels(): string[] {
|
||||
return Array.from(document.querySelectorAll(CM_OPTION_SELECTOR)).map(
|
||||
(el) => el.textContent ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
describe('QuerySearch context-prefixed key suggestions', () => {
|
||||
it('shows one deduped suggestion per key name in normal mode', async () => {
|
||||
await renderAndType('statu');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(visibleOptionLabels()).toContain('status.code');
|
||||
});
|
||||
|
||||
const statusOptions = visibleOptionLabels().filter(
|
||||
(label) => label === 'status.code',
|
||||
);
|
||||
expect(statusOptions).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('shows context-scoped suggestions for a complete context prefix', async () => {
|
||||
await renderAndType('attribute.');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(visibleOptionLabels()).toContain('attribute.status.code');
|
||||
});
|
||||
|
||||
const labels = visibleOptionLabels();
|
||||
// Literal key name match ranks first
|
||||
expect(labels[0]).toBe('attribute.a.b.c');
|
||||
// Context-qualified form of the literal key is also present
|
||||
expect(labels).toContain('attribute.attribute.a.b.c');
|
||||
// span-only key is not suggested under attribute.
|
||||
expect(labels).not.toContain('attribute.duration.nano');
|
||||
});
|
||||
|
||||
it('fetches the context page when the prefix is typed exactly', async () => {
|
||||
await renderAndType('attribute.');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchedSearchTexts).toContain('attribute.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('QuerySearch operator suggestions by key type', () => {
|
||||
it('shows all operators for a key with ambiguous types', async () => {
|
||||
// status.code is string + number across variants → no type filtering
|
||||
await typeKeyThenEnterOperatorContext('status.code', 'status.code');
|
||||
|
||||
const labels = visibleOptionLabels();
|
||||
expect(labels).toContain('>');
|
||||
expect(labels).toContain('LIKE');
|
||||
});
|
||||
|
||||
it('shows numeric operators for a single-type number key', async () => {
|
||||
await typeKeyThenEnterOperatorContext('duration.nano', 'duration.nano');
|
||||
|
||||
const labels = visibleOptionLabels();
|
||||
expect(labels).toContain('>');
|
||||
expect(labels).not.toContain('LIKE');
|
||||
});
|
||||
|
||||
it('narrows operators when a context prefix disambiguates the type', async () => {
|
||||
// status.code is ambiguous globally, but resource.status.code is string-only
|
||||
await typeKeyThenEnterOperatorContext(
|
||||
'resource.status.code',
|
||||
'resource.status.code',
|
||||
);
|
||||
|
||||
const labels = visibleOptionLabels();
|
||||
expect(labels).toContain('LIKE');
|
||||
expect(labels).not.toContain('>');
|
||||
});
|
||||
});
|
||||
@@ -8,13 +8,78 @@ import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/ty
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import QuerySearch from '../QuerySearch/QuerySearch';
|
||||
import { mockCodeMirrorDomApis } from './codemirrorDomMocks';
|
||||
|
||||
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
|
||||
|
||||
// Mock DOM APIs that CodeMirror needs
|
||||
beforeAll(() => {
|
||||
mockCodeMirrorDomApis();
|
||||
// Mock getClientRects and getBoundingClientRect for Range objects
|
||||
const mockRect: DOMRect = {
|
||||
width: 100,
|
||||
height: 20,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 100,
|
||||
bottom: 20,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: (): DOMRect => mockRect,
|
||||
} as DOMRect;
|
||||
|
||||
// Create a minimal Range mock with only what CodeMirror actually uses
|
||||
const createMockRange = (): Range => {
|
||||
let startContainer: Node = document.createTextNode('');
|
||||
let endContainer: Node = document.createTextNode('');
|
||||
let startOffset = 0;
|
||||
let endOffset = 0;
|
||||
|
||||
const mockRange = {
|
||||
// CodeMirror uses these for text measurement
|
||||
getClientRects: (): DOMRectList =>
|
||||
({
|
||||
length: 1,
|
||||
item: (index: number): DOMRect | null => (index === 0 ? mockRect : null),
|
||||
0: mockRect,
|
||||
*[Symbol.iterator](): Generator<DOMRect> {
|
||||
yield mockRect;
|
||||
},
|
||||
}) as unknown as DOMRectList,
|
||||
getBoundingClientRect: (): DOMRect => mockRect,
|
||||
// CodeMirror calls these to set up text ranges
|
||||
setStart: (node: Node, offset: number): void => {
|
||||
startContainer = node;
|
||||
startOffset = offset;
|
||||
},
|
||||
setEnd: (node: Node, offset: number): void => {
|
||||
endContainer = node;
|
||||
endOffset = offset;
|
||||
},
|
||||
// Minimal Range properties (TypeScript requires these)
|
||||
get startContainer(): Node {
|
||||
return startContainer;
|
||||
},
|
||||
get endContainer(): Node {
|
||||
return endContainer;
|
||||
},
|
||||
get startOffset(): number {
|
||||
return startOffset;
|
||||
},
|
||||
get endOffset(): number {
|
||||
return endOffset;
|
||||
},
|
||||
get collapsed(): boolean {
|
||||
return startContainer === endContainer && startOffset === endOffset;
|
||||
},
|
||||
commonAncestorContainer: document.body,
|
||||
};
|
||||
return mockRange as unknown as Range;
|
||||
};
|
||||
|
||||
// Mock document.createRange to return a new Range instance each time
|
||||
document.createRange = (): Range => createMockRange();
|
||||
|
||||
// Mock getBoundingClientRect for elements
|
||||
Element.prototype.getBoundingClientRect = (): DOMRect => mockRect;
|
||||
});
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// Mocks the DOM measurement APIs CodeMirror needs to render in jsdom
|
||||
// (Range client rects + element bounding rects). Call from a beforeAll in
|
||||
// specs that render the real CodeMirror editor.
|
||||
export function mockCodeMirrorDomApis(): void {
|
||||
const mockRect: DOMRect = {
|
||||
width: 100,
|
||||
height: 20,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 100,
|
||||
bottom: 20,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: (): DOMRect => mockRect,
|
||||
} as DOMRect;
|
||||
|
||||
// Create a minimal Range mock with only what CodeMirror actually uses
|
||||
const createMockRange = (): Range => {
|
||||
let startContainer: Node = document.createTextNode('');
|
||||
let endContainer: Node = document.createTextNode('');
|
||||
let startOffset = 0;
|
||||
let endOffset = 0;
|
||||
|
||||
const mockRange = {
|
||||
// CodeMirror uses these for text measurement
|
||||
getClientRects: (): DOMRectList =>
|
||||
({
|
||||
length: 1,
|
||||
item: (index: number): DOMRect | null => (index === 0 ? mockRect : null),
|
||||
0: mockRect,
|
||||
*[Symbol.iterator](): Generator<DOMRect> {
|
||||
yield mockRect;
|
||||
},
|
||||
}) as unknown as DOMRectList,
|
||||
getBoundingClientRect: (): DOMRect => mockRect,
|
||||
// CodeMirror calls these to set up text ranges
|
||||
setStart: (node: Node, offset: number): void => {
|
||||
startContainer = node;
|
||||
startOffset = offset;
|
||||
},
|
||||
setEnd: (node: Node, offset: number): void => {
|
||||
endContainer = node;
|
||||
endOffset = offset;
|
||||
},
|
||||
// Minimal Range properties (TypeScript requires these)
|
||||
get startContainer(): Node {
|
||||
return startContainer;
|
||||
},
|
||||
get endContainer(): Node {
|
||||
return endContainer;
|
||||
},
|
||||
get startOffset(): number {
|
||||
return startOffset;
|
||||
},
|
||||
get endOffset(): number {
|
||||
return endOffset;
|
||||
},
|
||||
get collapsed(): boolean {
|
||||
return startContainer === endContainer && startOffset === endOffset;
|
||||
},
|
||||
commonAncestorContainer: document.body,
|
||||
};
|
||||
return mockRange as unknown as Range;
|
||||
};
|
||||
|
||||
// Mock document.createRange to return a new Range instance each time
|
||||
document.createRange = (): Range => createMockRange();
|
||||
|
||||
// Mock getBoundingClientRect for elements
|
||||
Element.prototype.getBoundingClientRect = (): DOMRect => mockRect;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
.quick-filters-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
|
||||
.quick-filters-settings-container {
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import TagKeyValueInput from './TagKeyValueInput';
|
||||
|
||||
const TID = 'tag-key-value-input';
|
||||
|
||||
type User = ReturnType<typeof userEvent.setup>;
|
||||
|
||||
const startEditingFirstChip = async (user: User): Promise<HTMLElement> => {
|
||||
await user.dblClick(screen.getAllByTestId(`${TID}-chip`)[0]);
|
||||
return screen.getByTestId(`${TID}-edit`);
|
||||
};
|
||||
|
||||
describe('TagKeyValueInput — inline chip edit', () => {
|
||||
it('shows an error and stays in edit mode when Enter commits an invalid value', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onTagsChange = jest.fn();
|
||||
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
|
||||
|
||||
const input = await startEditingFirstChip(user);
|
||||
await user.clear(input);
|
||||
await user.type(input, 'novalue{Enter}');
|
||||
|
||||
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
|
||||
'key:value format',
|
||||
);
|
||||
// Still editing (input present), and no change committed.
|
||||
expect(screen.getByTestId(`${TID}-edit`)).toBeInTheDocument();
|
||||
expect(onTagsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows a duplicate error when Enter commits an existing tag', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onTagsChange = jest.fn();
|
||||
render(
|
||||
<TagKeyValueInput
|
||||
tags={['env:prod', 'team:pulse']}
|
||||
onTagsChange={onTagsChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = await startEditingFirstChip(user);
|
||||
await user.clear(input);
|
||||
await user.type(input, 'team:pulse{Enter}');
|
||||
|
||||
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
|
||||
'already exists',
|
||||
);
|
||||
expect(onTagsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('commits a valid edit on Enter', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onTagsChange = jest.fn();
|
||||
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
|
||||
|
||||
const input = await startEditingFirstChip(user);
|
||||
await user.clear(input);
|
||||
await user.type(input, 'env:staging{Enter}');
|
||||
|
||||
expect(onTagsChange).toHaveBeenCalledWith(['env:staging']);
|
||||
expect(screen.queryByTestId(`${TID}-error`)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('reverts silently (no error) when blurring an invalid edit', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onTagsChange = jest.fn();
|
||||
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
|
||||
|
||||
const input = await startEditingFirstChip(user);
|
||||
await user.clear(input);
|
||||
await user.type(input, 'novalue');
|
||||
await user.tab(); // blur off the edit input
|
||||
|
||||
expect(screen.queryByTestId(`${TID}-error`)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(`${TID}-edit`)).not.toBeInTheDocument();
|
||||
expect(onTagsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TagKeyValueInput — backend-rule validation', () => {
|
||||
it('rejects a key containing a space on inline edit', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onTagsChange = jest.fn();
|
||||
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
|
||||
|
||||
const input = await startEditingFirstChip(user);
|
||||
await user.clear(input);
|
||||
await user.type(input, 'my key:prod{Enter}');
|
||||
|
||||
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
|
||||
'spaces or special characters',
|
||||
);
|
||||
expect(screen.getByTestId(`${TID}-edit`)).toBeInTheDocument();
|
||||
expect(onTagsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a value containing a space in the new-tag input', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onTagsChange = jest.fn();
|
||||
render(<TagKeyValueInput tags={[]} onTagsChange={onTagsChange} />);
|
||||
|
||||
const input = screen.getByTestId(TID);
|
||||
await user.type(input, 'env:pro d{Enter}');
|
||||
|
||||
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
|
||||
'spaces or special characters',
|
||||
);
|
||||
expect(onTagsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
|
||||
import TagBadge from '../TagBadge/TagBadge';
|
||||
import { validateTag } from './utils';
|
||||
import { parseKeyValueTag } from './utils';
|
||||
|
||||
import styles from './TagKeyValueInput.module.scss';
|
||||
|
||||
@@ -43,12 +43,16 @@ function TagKeyValueInput({
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
const result = validateTag(raw, tags);
|
||||
if ('error' in result) {
|
||||
setError(result.error);
|
||||
const normalized = parseKeyValueTag(raw);
|
||||
if (!normalized) {
|
||||
setError('Tags must be in key:value format (both sides required).');
|
||||
return;
|
||||
}
|
||||
onTagsChange([...tags, result.tag]);
|
||||
if (tags.includes(normalized)) {
|
||||
setError('This tag already exists.');
|
||||
return;
|
||||
}
|
||||
onTagsChange([...tags, normalized]);
|
||||
setInputValue('');
|
||||
setError('');
|
||||
};
|
||||
@@ -78,20 +82,15 @@ function TagKeyValueInput({
|
||||
const cancelEdit = (): void => {
|
||||
setEditIndex(-1);
|
||||
setEditValue('');
|
||||
setError('');
|
||||
};
|
||||
|
||||
const commitEdit = (revertOnInvalid = false): void => {
|
||||
const result = validateTag(editValue, tags, editIndex);
|
||||
if ('error' in result) {
|
||||
if (revertOnInvalid) {
|
||||
cancelEdit();
|
||||
} else {
|
||||
setError(result.error);
|
||||
}
|
||||
return;
|
||||
const commitEdit = (): void => {
|
||||
const normalized = parseKeyValueTag(editValue);
|
||||
// Drop into a no-op (revert) on invalid or duplicate edits rather than
|
||||
// stranding the user in an un-exitable edit box.
|
||||
if (normalized && !tags.some((t, i) => t === normalized && i !== editIndex)) {
|
||||
onTagsChange(tags.map((t, i) => (i === editIndex ? normalized : t)));
|
||||
}
|
||||
onTagsChange(tags.map((t, i) => (i === editIndex ? result.tag : t)));
|
||||
cancelEdit();
|
||||
};
|
||||
|
||||
@@ -122,14 +121,11 @@ function TagKeyValueInput({
|
||||
value={editValue}
|
||||
autoFocus
|
||||
testId={`${testId}-edit`}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setEditValue(e.target.value);
|
||||
if (error) {
|
||||
setError('');
|
||||
}
|
||||
}}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
|
||||
setEditValue(e.target.value)
|
||||
}
|
||||
onKeyDown={handleEditKeyDown}
|
||||
onBlur={(): void => commitEdit(true)}
|
||||
onBlur={commitEdit}
|
||||
/>
|
||||
) : (
|
||||
<TagBadge
|
||||
|
||||
@@ -15,42 +15,3 @@ export function parseKeyValueTag(raw: string): string | null {
|
||||
}
|
||||
return `${key}:${value}`;
|
||||
}
|
||||
|
||||
const TAG_KEY_REGEX = new RegExp('^[a-zA-Z$_@{#][a-zA-Z0-9$_@#{}:/-]*$');
|
||||
const TAG_VALUE_REGEX = new RegExp('^[a-zA-Z0-9$_@#{}:.+=/-]*$');
|
||||
const MAX_TAG_LEN = 32;
|
||||
|
||||
export type TagValidation = { tag: string } | { error: string };
|
||||
|
||||
export function validateTag(
|
||||
raw: string,
|
||||
existingTags: string[],
|
||||
excludeIndex = -1,
|
||||
): TagValidation {
|
||||
const normalized = parseKeyValueTag(raw);
|
||||
if (!normalized) {
|
||||
return { error: 'Tags must be in key:value format (both sides required).' };
|
||||
}
|
||||
const separator = normalized.indexOf(':');
|
||||
const key = normalized.slice(0, separator);
|
||||
const value = normalized.slice(separator + 1);
|
||||
if (!TAG_KEY_REGEX.test(key)) {
|
||||
return { error: 'Tag keys cannot contain spaces or special characters.' };
|
||||
}
|
||||
if (!TAG_VALUE_REGEX.test(value)) {
|
||||
return { error: 'Tag values cannot contain spaces or special characters.' };
|
||||
}
|
||||
if (key.length > MAX_TAG_LEN || value.length > MAX_TAG_LEN) {
|
||||
return {
|
||||
error: `Tag key and value must each be ${MAX_TAG_LEN} characters or fewer.`,
|
||||
};
|
||||
}
|
||||
if (
|
||||
existingTags.some(
|
||||
(tag, index) => tag === normalized && index !== excludeIndex,
|
||||
)
|
||||
) {
|
||||
return { error: 'This tag already exists.' };
|
||||
}
|
||||
return { tag: normalized };
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { ComponentProps, memo, useCallback, useLayoutEffect } from 'react';
|
||||
import { ComponentProps, memo } from 'react';
|
||||
import { TableComponents } from 'react-virtuoso';
|
||||
import cx from 'classnames';
|
||||
|
||||
import {
|
||||
chromePerformanceTanstackTableBeginHover,
|
||||
chromePerformanceMeasureTanstackTable,
|
||||
} from './perfDevtools';
|
||||
import TanStackRowCells from './TanStackRow';
|
||||
import {
|
||||
useClearRowHovered,
|
||||
@@ -14,7 +10,6 @@ import {
|
||||
import { FlatItem, TableRowContext } from './types';
|
||||
|
||||
import tableStyles from './TanStackTable.module.scss';
|
||||
import { chromePerformanceNow } from 'lib/chromePerformanceDevTools';
|
||||
|
||||
type VirtuosoTableRowProps<TData, TItemKey = string> = ComponentProps<
|
||||
NonNullable<
|
||||
@@ -27,7 +22,6 @@ function TanStackCustomTableRow<TData, TItemKey = string>({
|
||||
context,
|
||||
...props
|
||||
}: VirtuosoTableRowProps<TData, TItemKey>): JSX.Element {
|
||||
const renderStart = chromePerformanceNow();
|
||||
const rowId = item.row.id;
|
||||
const rowData = item.row.original;
|
||||
|
||||
@@ -35,23 +29,6 @@ function TanStackCustomTableRow<TData, TItemKey = string>({
|
||||
const setHovered = useSetRowHovered(rowId);
|
||||
const clearHovered = useClearRowHovered(rowId);
|
||||
|
||||
const handleMouseEnter = useCallback((): void => {
|
||||
chromePerformanceTanstackTableBeginHover(rowId);
|
||||
setHovered();
|
||||
}, [rowId, setHovered]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
chromePerformanceMeasureTanstackTable('Row render', renderStart, {
|
||||
track: 'Row render',
|
||||
color: 'secondary',
|
||||
tooltipText: 'Row render + commit',
|
||||
properties: [
|
||||
['rowId', rowId],
|
||||
['kind', item.kind],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
if (item.kind === 'expansion') {
|
||||
return (
|
||||
<tr {...props} className={tableStyles.tableRowExpansion}>
|
||||
@@ -88,7 +65,7 @@ function TanStackCustomTableRow<TData, TItemKey = string>({
|
||||
{...props}
|
||||
className={rowClassName}
|
||||
style={rowStyle}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseEnter={setHovered}
|
||||
onMouseLeave={clearHovered}
|
||||
>
|
||||
<TanStackRowCells
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useLayoutEffect, type ReactNode } from 'react';
|
||||
|
||||
import { chromePerformanceTanstackTableEndHover } from './perfDevtools';
|
||||
import { useIsRowHovered } from './TanStackTableStateContext';
|
||||
import { TooltipSimple, TooltipSimpleProps } from '@signozhq/ui/tooltip';
|
||||
|
||||
export type HoverTooltipProps = Omit<TooltipSimpleProps, 'open'> & {
|
||||
rowId: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function TanStackHoverTooltip({
|
||||
rowId,
|
||||
children,
|
||||
...tooltipProps
|
||||
}: HoverTooltipProps): JSX.Element {
|
||||
const isHovered = useIsRowHovered(rowId);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isHovered) {
|
||||
chromePerformanceTanstackTableEndHover(rowId);
|
||||
}
|
||||
}, [isHovered, rowId]);
|
||||
|
||||
if (!isHovered) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return <TooltipSimple {...tooltipProps}>{children}</TooltipSimple>;
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
@@ -45,7 +44,6 @@ import {
|
||||
TanStackTableHandle,
|
||||
TanStackTableProps,
|
||||
} from './types';
|
||||
import { chromePerformanceMeasureTanstackTable } from './perfDevtools';
|
||||
import { useColumnDnd } from './useColumnDnd';
|
||||
import { useColumnHandlers } from './useColumnHandlers';
|
||||
import { useColumnState } from './useColumnState';
|
||||
@@ -58,7 +56,6 @@ import { VirtuosoTableColGroup } from './VirtuosoTableColGroup';
|
||||
|
||||
import tableStyles from './TanStackTable.module.scss';
|
||||
import viewStyles from './TanStackTableView.module.scss';
|
||||
import { chromePerformanceNow } from 'lib/chromePerformanceDevTools';
|
||||
|
||||
const COLUMN_DND_AUTO_SCROLL = {
|
||||
layoutShiftCompensation: false as const,
|
||||
@@ -119,8 +116,6 @@ function TanStackTableInner<TData, TItemKey = string>(
|
||||
);
|
||||
}
|
||||
|
||||
const renderStart = chromePerformanceNow();
|
||||
|
||||
const virtuosoRef = useRef<TableVirtuosoHandle | null>(null);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@@ -349,19 +344,6 @@ function TanStackTableInner<TData, TItemKey = string>(
|
||||
|
||||
const visibleColumnsCount = table.getVisibleFlatColumns().length;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
chromePerformanceMeasureTanstackTable('Table render', renderStart, {
|
||||
track: 'Table render',
|
||||
color: 'primary',
|
||||
tooltipText: 'TanStackTable render + commit',
|
||||
properties: [
|
||||
['rows', String(flatItems.length)],
|
||||
['columns', String(visibleColumnsCount)],
|
||||
['loading', String(isLoading)],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const columnOrderKey = useMemo(() => columnIds.join(','), [columnIds]);
|
||||
const columnVisibilityKey = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { TanStackHoverTooltip } from './TanStackHoverTooltip';
|
||||
import { TanStackTableBase } from './TanStackTable';
|
||||
import TanStackTableText from './TanStackTableText';
|
||||
|
||||
export * from './TanStackHoverTooltip';
|
||||
export * from './TanStackTableStateContext';
|
||||
export * from './types';
|
||||
export * from './useCalculatedPageSize';
|
||||
@@ -276,7 +274,6 @@ export * from './useTableParams';
|
||||
*/
|
||||
const TanStackTable = Object.assign(TanStackTableBase, {
|
||||
Text: TanStackTableText,
|
||||
HoverTooltip: TanStackHoverTooltip,
|
||||
});
|
||||
|
||||
export default TanStackTable;
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import {
|
||||
createChromePerformanceInteractionTracker,
|
||||
chromePerformanceMeasure,
|
||||
ChromePerformanceTrackEntryOptions,
|
||||
} from 'lib/chromePerformanceDevTools';
|
||||
|
||||
const TABLE_TRACK_GROUP = 'SigNoz Table';
|
||||
|
||||
export function chromePerformanceMeasureTanstackTable(
|
||||
name: string,
|
||||
start: number,
|
||||
{
|
||||
track,
|
||||
color,
|
||||
tooltipText,
|
||||
properties,
|
||||
}: Omit<ChromePerformanceTrackEntryOptions, 'trackGroup'>,
|
||||
): void {
|
||||
chromePerformanceMeasure(name, start, {
|
||||
trackGroup: TABLE_TRACK_GROUP,
|
||||
track,
|
||||
color,
|
||||
tooltipText,
|
||||
properties,
|
||||
});
|
||||
}
|
||||
|
||||
const hoverTracker = createChromePerformanceInteractionTracker(
|
||||
TABLE_TRACK_GROUP,
|
||||
'Hover',
|
||||
'Row hover',
|
||||
);
|
||||
|
||||
export const chromePerformanceTanstackTableBeginHover = hoverTracker.begin;
|
||||
export const chromePerformanceTanstackTableEndHover = hoverTracker.end;
|
||||
@@ -21,7 +21,6 @@ export type TableCellContext<TData, TValue> = {
|
||||
value: TValue;
|
||||
isActive: boolean;
|
||||
rowIndex: number;
|
||||
rowId: string;
|
||||
isExpanded: boolean;
|
||||
canExpand: boolean;
|
||||
toggleExpanded: () => void;
|
||||
|
||||
@@ -135,7 +135,6 @@ export function buildTanstackColumnDef<TData, TItemKey = string>(
|
||||
value: getValue() as TData[any],
|
||||
isActive: isRowActive?.(rowData) ?? false,
|
||||
rowIndex: row.index,
|
||||
rowId: row.id,
|
||||
isExpanded: row.getIsExpanded(),
|
||||
canExpand: row.getCanExpand(),
|
||||
toggleExpanded: (): void => {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { MemoryRouter, useHistory } from 'react-router-dom';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { INITIAL_CREATE_ALERT_STATE } from '../constants';
|
||||
import { CreateAlertProvider, useCreateAlertState } from '../index';
|
||||
import { AlertThresholdMatchType } from '../types';
|
||||
|
||||
// The provider only needs a query with a unit + empty builder for these assertions.
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
@@ -110,67 +108,3 @@ describe('CreateAlertProvider — URL-declared prefill (issue #5291)', () => {
|
||||
expect(screen.getByTestId('window-type')).toHaveTextContent('cumulative');
|
||||
});
|
||||
});
|
||||
|
||||
// Reproduces the #12137 regression: on the alert overview page the query builder
|
||||
// rewrites location.search after the alert loads, which used to re-run the prefill
|
||||
// effect and RESET the loaded threshold back to 0.
|
||||
function SearchMutator(): JSX.Element {
|
||||
const routerHistory = useHistory();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="mutate-search"
|
||||
onClick={(): void =>
|
||||
routerHistory.replace(
|
||||
'/alerts/overview?compositeQuery=normalized&ruleId=r1',
|
||||
)
|
||||
}
|
||||
>
|
||||
change search
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
describe('CreateAlertProvider — edit mode ignores URL prefill', () => {
|
||||
it('keeps loaded thresholds when the query builder rewrites location.search', () => {
|
||||
const initialAlertState = {
|
||||
...INITIAL_CREATE_ALERT_STATE,
|
||||
threshold: {
|
||||
...INITIAL_CREATE_ALERT_STATE.threshold,
|
||||
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
thresholds: [
|
||||
{
|
||||
...INITIAL_CREATE_ALERT_STATE.threshold.thresholds[0],
|
||||
thresholdValue: 245,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/alerts/overview?ruleId=r1']}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CreateAlertProvider
|
||||
initialAlertType={AlertTypes.METRICS_BASED_ALERT}
|
||||
isEditMode
|
||||
ruleId="r1"
|
||||
initialAlertState={initialAlertState}
|
||||
>
|
||||
<Probe />
|
||||
<SearchMutator />
|
||||
</CreateAlertProvider>
|
||||
</QueryClientProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('threshold-value')).toHaveTextContent('245');
|
||||
|
||||
// Simulate the query builder normalizing the query into the URL post-load.
|
||||
fireEvent.click(screen.getByTestId('mutate-search'));
|
||||
|
||||
expect(screen.getByTestId('threshold-value')).toHaveTextContent('245');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,14 +168,6 @@ export function CreateAlertProvider(
|
||||
const yAxisUnitAppliedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// URL-declared prefill is a create-flow concern. In edit mode the alert
|
||||
// state is owned by SET_INITIAL_STATE; running the RESET below would wipe
|
||||
// the loaded thresholds each time the query builder rewrites location.search
|
||||
// (which yields a fresh urlPrefill object and re-triggers this effect).
|
||||
if (isEditMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.THRESHOLD,
|
||||
action: {
|
||||
@@ -243,7 +235,7 @@ export function CreateAlertProvider(
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [alertType, urlPrefill, ruleNameFromURL, yAxisUnitFromURL, isEditMode]);
|
||||
}, [alertType, urlPrefill, ruleNameFromURL, yAxisUnitFromURL]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode && initialAlertState) {
|
||||
|
||||
@@ -51,7 +51,6 @@ import { getHostsQuickFiltersConfig } from './utils';
|
||||
import styles from './InfraMonitoringHosts.module.scss';
|
||||
import { ArrowUpToLine, Filter } from '@signozhq/icons';
|
||||
import { NANO_SECOND_MULTIPLIER, useGlobalTimeStore } from 'store/globalTime';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
|
||||
function Hosts(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
@@ -212,31 +211,27 @@ function Hosts(): JSX.Element {
|
||||
<div className={styles.infraContentRow}>
|
||||
{showFilters && (
|
||||
<div className={styles.quickFiltersContainer}>
|
||||
<OverlayScrollbar>
|
||||
<>
|
||||
<div className={styles.quickFiltersContainerHeader}>
|
||||
<Typography.Text>Filters</Typography.Text>
|
||||
<Tooltip title="Collapse Filters">
|
||||
<ArrowUpToLine
|
||||
style={{ rotate: '270deg', cursor: 'pointer' }}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
size="md"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={getHostsQuickFiltersConfig(dotMetricsEnabled)}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={{
|
||||
metricNamespace:
|
||||
METRIC_NAMESPACE_BY_ENTITY[InfraMonitoringEntity.HOSTS],
|
||||
startUnixMilli,
|
||||
endUnixMilli,
|
||||
}}
|
||||
<div className={styles.quickFiltersContainerHeader}>
|
||||
<Typography.Text>Filters</Typography.Text>
|
||||
<Tooltip title="Collapse Filters">
|
||||
<ArrowUpToLine
|
||||
style={{ rotate: '270deg', cursor: 'pointer' }}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
size="md"
|
||||
/>
|
||||
</>
|
||||
</OverlayScrollbar>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={getHostsQuickFiltersConfig(dotMetricsEnabled)}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={{
|
||||
metricNamespace:
|
||||
METRIC_NAMESPACE_BY_ENTITY[InfraMonitoringEntity.HOSTS],
|
||||
startUnixMilli,
|
||||
endUnixMilli,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
@@ -253,7 +248,6 @@ function Hosts(): JSX.Element {
|
||||
getRowKey={getHostRowKey}
|
||||
getItemKey={getHostItemKey}
|
||||
eventCategory={InfraMonitoringEvents.HostEntity}
|
||||
detailsQueryKeyPrefix="hosts"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,6 +48,24 @@
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
|
||||
}
|
||||
|
||||
:global(.ant-collapse-header) {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
@@ -62,6 +80,28 @@
|
||||
padding-left: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.quick-filters) {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quickFiltersContainerHeader {
|
||||
|
||||
@@ -130,13 +130,12 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
accessorFn: (row): string => row.status,
|
||||
width: { min: 140 },
|
||||
enableSort: false,
|
||||
cell: ({ value, groupMeta, row, rowId }): React.ReactNode => {
|
||||
cell: ({ value, groupMeta, row }): React.ReactNode => {
|
||||
const status = value as InframonitoringtypesHostStatusDTO;
|
||||
|
||||
if (groupMeta) {
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
rowId={rowId}
|
||||
items={[
|
||||
{
|
||||
value: row.activeHostCount,
|
||||
@@ -196,13 +195,13 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
<ColumnHeader
|
||||
tooltip="Excluding cache memory."
|
||||
docPath="/infrastructure-monitoring/host-monitoring#memory-usage"
|
||||
className={styles.memoryUsageHeader}
|
||||
>
|
||||
Memory Usage
|
||||
<br /> (WSS)
|
||||
Memory Usage (WSS)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.memory,
|
||||
width: { min: 200 },
|
||||
width: { min: 240 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
@@ -249,12 +248,11 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
id: 'load15',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#load-avg">
|
||||
Load Avg
|
||||
<br /> (15min)
|
||||
Load Avg (15min)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.load15,
|
||||
width: { min: 160 },
|
||||
width: { min: 200 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const load15 = Number(value);
|
||||
@@ -278,7 +276,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.diskUsage,
|
||||
width: { min: 200 },
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const diskUsage = value as number;
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.memoryUsageHeader {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-2);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.statusTag {
|
||||
width: fit-content;
|
||||
font-family: Inter, sans-serif;
|
||||
|
||||
@@ -1,39 +1,148 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { X } from '@signozhq/icons';
|
||||
import { Button, Drawer, Tooltip } from 'antd';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Drawer } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||
import APIError from 'types/api/error';
|
||||
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
GlobalTimeProvider,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
useGlobalTimeStore,
|
||||
} from 'store/globalTime';
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import {
|
||||
BarChart,
|
||||
ChevronsLeftRight,
|
||||
Compass,
|
||||
DraftingCompass,
|
||||
ScrollText,
|
||||
X,
|
||||
} from '@signozhq/icons';
|
||||
import { isCustomTimeRange, useGlobalTimeStore } from 'store/globalTime';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import {
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from '../constants';
|
||||
import { useInfraMonitoringSelectedItemParams } from '../hooks';
|
||||
import { InfraMonitoringEntity, VIEW_TYPES } from '../constants';
|
||||
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
|
||||
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
|
||||
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityLogs/hooks';
|
||||
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
|
||||
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
|
||||
import {
|
||||
SelectedItemParams,
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringSelectedItemParams,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
|
||||
import K8sBaseDetailsContent from './K8sBaseDetailsContent';
|
||||
import { K8sBaseDetailsProps } from './types';
|
||||
|
||||
import '../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import {
|
||||
EntityCountConfig,
|
||||
EntityCountsSection,
|
||||
} from './components/EntityCountsSection/EntityCountsSection';
|
||||
|
||||
export type {
|
||||
CustomTab,
|
||||
CustomTabRenderProps,
|
||||
K8sBaseDetailsProps,
|
||||
K8sDetailsCountConfig,
|
||||
K8sDetailsFilters,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from './types';
|
||||
const TimeRangeOffset = 1000000000;
|
||||
|
||||
export interface K8sDetailsMetadataConfig<T> {
|
||||
label: string;
|
||||
getValue: (entity: T) => string | number;
|
||||
render?: (value: string | number, entity: T) => React.ReactNode;
|
||||
}
|
||||
|
||||
export type K8sDetailsCountConfig<T> = EntityCountConfig<T>;
|
||||
|
||||
export interface K8sDetailsFilters {
|
||||
filter: { expression: string };
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface CustomTabRenderProps<T> {
|
||||
entity: T;
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: Time;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface CustomTab<T> {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
render: (props: CustomTabRenderProps<T>) => React.ReactNode;
|
||||
}
|
||||
|
||||
export interface K8sBaseDetailsProps<T> {
|
||||
category: InfraMonitoringEntity;
|
||||
eventCategory: string;
|
||||
// Data fetching configuration
|
||||
getSelectedItemExpression: (params: SelectedItemParams) => string;
|
||||
fetchEntityData: (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{ data: T | null; error?: APIError | null }>;
|
||||
// Entity configuration
|
||||
getEntityName: (entity: T) => string;
|
||||
getInitialLogTracesExpression: (entity: T) => string;
|
||||
getInitialEventsExpression: (entity: T) => string;
|
||||
metadataConfig: K8sDetailsMetadataConfig<T>[];
|
||||
countsConfig?: K8sDetailsCountConfig<T>[];
|
||||
getCountsFilterExpression?: (entity: T) => string;
|
||||
entityWidgetInfo: {
|
||||
title: string;
|
||||
yAxisUnit: string;
|
||||
docPath?: string;
|
||||
}[];
|
||||
getEntityQueryPayload: (
|
||||
entity: T,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
) => GetQueryResultsProps[];
|
||||
queryKeyPrefix: string;
|
||||
/** When true, only metrics are shown and the Metrics/Logs/Traces/Events tab bar is hidden. */
|
||||
hideDetailViewTabs?: boolean;
|
||||
tabsConfig?: {
|
||||
showMetrics?: boolean;
|
||||
showLogs?: boolean;
|
||||
showTraces?: boolean;
|
||||
showEvents?: boolean;
|
||||
};
|
||||
customTabs?: Array<CustomTab<T>>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function K8sBaseDetails<T>({
|
||||
category,
|
||||
eventCategory,
|
||||
@@ -54,6 +163,7 @@ export default function K8sBaseDetails<T>({
|
||||
}: K8sBaseDetailsProps<T>): JSX.Element {
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
const lastComputedMinMax = useGlobalTimeStore((s) => s.lastComputedMinMax);
|
||||
const getAutoRefreshQueryKey = useGlobalTimeStore(
|
||||
(s) => s.getAutoRefreshQueryKey,
|
||||
);
|
||||
@@ -127,9 +237,86 @@ export default function K8sBaseDetails<T>({
|
||||
|
||||
const entityName = entity ? getEntityName(entity) : '';
|
||||
|
||||
// Content state (previously in K8sBaseDetailsContent)
|
||||
const tabVisibility = useMemo(
|
||||
() => ({
|
||||
showMetrics: true,
|
||||
showLogs: true,
|
||||
showTraces: true,
|
||||
showEvents: true,
|
||||
...tabsConfig,
|
||||
}),
|
||||
[tabsConfig],
|
||||
);
|
||||
|
||||
const { startMs, endMs } = useMemo(
|
||||
() => ({
|
||||
startMs: Math.floor(lastComputedMinMax.minTime / NANO_SECOND_MULTIPLIER),
|
||||
endMs: Math.floor(lastComputedMinMax.maxTime / NANO_SECOND_MULTIPLIER),
|
||||
}),
|
||||
[lastComputedMinMax],
|
||||
);
|
||||
|
||||
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
||||
startTime: startMs,
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
// TODO(h4ad): Remove this and use context/zustand
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: isCustomTimeRange(selectedTime)
|
||||
? DEFAULT_TIME_RANGE
|
||||
: selectedTime,
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
|
||||
|
||||
const validTabs = useMemo(() => {
|
||||
const tabs: string[] = [];
|
||||
if (tabVisibility.showMetrics) {
|
||||
tabs.push(VIEW_TYPES.METRICS);
|
||||
}
|
||||
if (tabVisibility.showLogs) {
|
||||
tabs.push(VIEW_TYPES.LOGS);
|
||||
}
|
||||
if (tabVisibility.showTraces) {
|
||||
tabs.push(VIEW_TYPES.TRACES);
|
||||
}
|
||||
if (tabVisibility.showEvents) {
|
||||
tabs.push(VIEW_TYPES.EVENTS);
|
||||
}
|
||||
if (customTabs) {
|
||||
tabs.push(...customTabs.map((t) => t.key));
|
||||
}
|
||||
return tabs;
|
||||
}, [tabVisibility, customTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hideDetailViewTabs && !validTabs.includes(selectedView)) {
|
||||
const firstValid = validTabs[0] || VIEW_TYPES.METRICS;
|
||||
void setSelectedView(firstValid);
|
||||
}
|
||||
}, [hideDetailViewTabs, selectedView, validTabs, setSelectedView]);
|
||||
|
||||
const [, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [, setTracesFiltersParam] = useInfraMonitoringTracesFilters();
|
||||
const [, setEventsFiltersParam] = useInfraMonitoringEventsFilters();
|
||||
const [userLogsExpression] = useQueryState(
|
||||
K8S_ENTITY_LOGS_EXPRESSION_KEY,
|
||||
parseAsString,
|
||||
);
|
||||
const [userTracesExpression] = useQueryState(
|
||||
K8S_ENTITY_TRACES_EXPRESSION_KEY,
|
||||
parseAsString,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (entity) {
|
||||
void logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
@@ -137,6 +324,136 @@ export default function K8sBaseDetails<T>({
|
||||
}
|
||||
}, [entity, eventCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
if (!isCustomTimeRange(currentSelectedInterval)) {
|
||||
setSelectedInterval(currentSelectedInterval);
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
}
|
||||
}, [getMinMaxTime, selectedTime]);
|
||||
|
||||
const handleTabChange = (value: string | null): void => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
setSelectedView(value);
|
||||
setLogFiltersParam(null);
|
||||
setTracesFiltersParam(null);
|
||||
setEventsFiltersParam(null);
|
||||
logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(dateTimeRange[0] / 1000),
|
||||
endTime: Math.floor(dateTimeRange[1] / 1000),
|
||||
});
|
||||
} else {
|
||||
const { maxTime, minTime } = GetMinMax(interval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
interval,
|
||||
view: effectiveView,
|
||||
});
|
||||
},
|
||||
[eventCategory, effectiveView],
|
||||
);
|
||||
|
||||
const handleExplorePagesRedirect = (): void => {
|
||||
const urlQuery = new URLSearchParams();
|
||||
|
||||
if (selectedInterval !== 'custom') {
|
||||
urlQuery.set(QueryParams.relativeTime, selectedInterval);
|
||||
} else {
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
urlQuery.set(QueryParams.startTime, modalTimeRange.startTime.toString());
|
||||
urlQuery.set(QueryParams.endTime, modalTimeRange.endTime.toString());
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ExploreClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const fullExpression = combineInitialAndUserExpression(
|
||||
logsAndTracesInitialExpression,
|
||||
userLogsExpression || '',
|
||||
);
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.logs,
|
||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||
expression: fullExpression,
|
||||
filter: { expression: fullExpression },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const fullExpression = combineInitialAndUserExpression(
|
||||
logsAndTracesInitialExpression,
|
||||
userTracesExpression || '',
|
||||
);
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
aggregateOperator: TracesAggregatorOperator.NOOP,
|
||||
expression: fullExpression,
|
||||
filter: { expression: fullExpression },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="70%"
|
||||
@@ -163,9 +480,8 @@ export default function K8sBaseDetails<T>({
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{(isEntityLoading || !selectedItem) && <LoadingContainer />}
|
||||
|
||||
{selectedItem && (isEntityError || hasResponseError) && (
|
||||
{isEntityLoading && <LoadingContainer />}
|
||||
{(isEntityError || hasResponseError) && (
|
||||
<div className="entity-error-container">
|
||||
<ErrorContent
|
||||
error={
|
||||
@@ -181,34 +497,208 @@ export default function K8sBaseDetails<T>({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{selectedItem && entity && !isEntityLoading && !hasResponseError && (
|
||||
<GlobalTimeProvider
|
||||
inheritGlobalTime
|
||||
enableUrlParams={{
|
||||
relativeTimeKey: INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_RELATIVE_TIME,
|
||||
startTimeKey: INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_START_TIME,
|
||||
endTimeKey: INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_END_TIME,
|
||||
}}
|
||||
>
|
||||
<K8sBaseDetailsContent<T>
|
||||
entity={entity}
|
||||
category={category}
|
||||
eventCategory={eventCategory}
|
||||
metadataConfig={metadataConfig}
|
||||
countsConfig={countsConfig}
|
||||
getCountsFilterExpression={getCountsFilterExpression}
|
||||
selectedItem={selectedItem}
|
||||
handleClose={handleClose}
|
||||
entityWidgetInfo={entityWidgetInfo}
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
queryKeyPrefix={queryKeyPrefix}
|
||||
hideDetailViewTabs={hideDetailViewTabs}
|
||||
tabsConfig={tabsConfig}
|
||||
customTabs={customTabs}
|
||||
logsAndTracesInitialExpression={logsAndTracesInitialExpression}
|
||||
eventsInitialExpression={eventsInitialExpression}
|
||||
/>
|
||||
</GlobalTimeProvider>
|
||||
{entity && !isEntityLoading && !hasResponseError && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
{metadataConfig.map((config) => (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
color="muted"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
{config.label}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="values-row">
|
||||
{metadataConfig.map((config) => {
|
||||
const value = config.getValue(entity);
|
||||
const displayValue = String(value);
|
||||
return (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
className="entity-details-metadata-value"
|
||||
>
|
||||
{config.render ? (
|
||||
config.render(value, entity)
|
||||
) : (
|
||||
<Tooltip title={displayValue}>{displayValue}</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{countsConfig &&
|
||||
countsConfig.length > 0 &&
|
||||
selectedItem &&
|
||||
getCountsFilterExpression && (
|
||||
<EntityCountsSection
|
||||
entity={entity}
|
||||
countsConfig={countsConfig}
|
||||
selectedItem={selectedItem}
|
||||
filterExpression={getCountsFilterExpression(entity)}
|
||||
closeDrawer={handleClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!hideDetailViewTabs && (
|
||||
<div className="views-tabs-container">
|
||||
<ToggleGroupSimple
|
||||
type="single"
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
items={[
|
||||
...(tabVisibility.showMetrics
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.METRICS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<BarChart size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showLogs
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.LOGS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showTraces
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.TRACES,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<DraftingCompass size={14} />
|
||||
Traces
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showEvents
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.EVENTS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Events
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(customTabs?.map((tab) => ({
|
||||
value: tab.key,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</div>
|
||||
),
|
||||
})) ?? []),
|
||||
]}
|
||||
/>
|
||||
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<Tooltip title="Go to Logs Explorer" placement="left">
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<Tooltip title="Go to Traces Explorer" placement="left">
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{effectiveView === VIEW_TYPES.METRICS && (
|
||||
<EntityMetrics<T>
|
||||
entity={entity}
|
||||
selectedInterval={selectedInterval}
|
||||
timeRange={modalTimeRange}
|
||||
handleTimeChange={handleTimeChange}
|
||||
isModalTimeSelection
|
||||
entityWidgetInfo={entityWidgetInfo}
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
category={category}
|
||||
queryKey={`${queryKeyPrefix}Metrics`}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.LOGS && (
|
||||
<EntityLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey={`${queryKeyPrefix}Logs`}
|
||||
category={category}
|
||||
initialExpression={logsAndTracesInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.TRACES && (
|
||||
<EntityTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey={`${queryKeyPrefix}Traces`}
|
||||
category={category}
|
||||
initialExpression={logsAndTracesInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.EVENTS && tabVisibility.showEvents && (
|
||||
<EntityEvents
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
category={category}
|
||||
queryKey={`${queryKeyPrefix}Events`}
|
||||
initialExpression={eventsInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{customTabs?.map((tab) =>
|
||||
selectedView === tab.key ? (
|
||||
<React.Fragment key={tab.key}>
|
||||
{tab.render({
|
||||
entity,
|
||||
timeRange: modalTimeRange,
|
||||
selectedInterval,
|
||||
handleTimeChange,
|
||||
})}
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
@@ -1,401 +0,0 @@
|
||||
import { Fragment, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
BarChart,
|
||||
ChevronsLeftRight,
|
||||
Compass,
|
||||
DraftingCompass,
|
||||
ScrollText,
|
||||
} from '@signozhq/icons';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import {
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { VIEW_TYPES } from '../constants';
|
||||
import { useEntityDetailsTime } from '../EntityDetailsUtils/EntityDateTimeSelector/useEntityDetailsTime';
|
||||
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
|
||||
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
|
||||
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityLogs/hooks';
|
||||
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
|
||||
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
|
||||
import {
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
|
||||
import { EntityCountsSection } from './components/EntityCountsSection/EntityCountsSection';
|
||||
import { K8sBaseDetailsContentProps } from './types';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function K8sBaseDetailsContent<T>({
|
||||
entity,
|
||||
category,
|
||||
eventCategory,
|
||||
metadataConfig,
|
||||
countsConfig,
|
||||
getCountsFilterExpression,
|
||||
selectedItem,
|
||||
handleClose,
|
||||
entityWidgetInfo,
|
||||
getEntityQueryPayload,
|
||||
queryKeyPrefix,
|
||||
hideDetailViewTabs,
|
||||
tabsConfig,
|
||||
customTabs,
|
||||
logsAndTracesInitialExpression,
|
||||
eventsInitialExpression,
|
||||
}: K8sBaseDetailsContentProps<T>): JSX.Element {
|
||||
const { timeRange, selectedInterval, handleTimeChange } =
|
||||
useEntityDetailsTime();
|
||||
|
||||
const tabVisibility = useMemo(
|
||||
() => ({
|
||||
showMetrics: true,
|
||||
showLogs: true,
|
||||
showTraces: true,
|
||||
showEvents: true,
|
||||
...tabsConfig,
|
||||
}),
|
||||
[tabsConfig],
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
|
||||
const validTabs = useMemo(() => {
|
||||
const tabs: string[] = [];
|
||||
if (tabVisibility.showMetrics) {
|
||||
tabs.push(VIEW_TYPES.METRICS);
|
||||
}
|
||||
if (tabVisibility.showLogs) {
|
||||
tabs.push(VIEW_TYPES.LOGS);
|
||||
}
|
||||
if (tabVisibility.showTraces) {
|
||||
tabs.push(VIEW_TYPES.TRACES);
|
||||
}
|
||||
if (tabVisibility.showEvents) {
|
||||
tabs.push(VIEW_TYPES.EVENTS);
|
||||
}
|
||||
if (customTabs) {
|
||||
tabs.push(...customTabs.map((t) => t.key));
|
||||
}
|
||||
return tabs;
|
||||
}, [tabVisibility, customTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hideDetailViewTabs && !validTabs.includes(selectedView)) {
|
||||
const firstValid = validTabs[0] || VIEW_TYPES.METRICS;
|
||||
void setSelectedView(firstValid);
|
||||
}
|
||||
}, [hideDetailViewTabs, selectedView, validTabs, setSelectedView]);
|
||||
|
||||
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
|
||||
|
||||
const [, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [, setTracesFiltersParam] = useInfraMonitoringTracesFilters();
|
||||
const [, setEventsFiltersParam] = useInfraMonitoringEventsFilters();
|
||||
const [userLogsExpression] = useQueryState(
|
||||
K8S_ENTITY_LOGS_EXPRESSION_KEY,
|
||||
parseAsString,
|
||||
);
|
||||
const [userTracesExpression] = useQueryState(
|
||||
K8S_ENTITY_TRACES_EXPRESSION_KEY,
|
||||
parseAsString,
|
||||
);
|
||||
|
||||
const handleTabChange = (value: string | null): void => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
void setSelectedView(value);
|
||||
void setLogFiltersParam(null);
|
||||
void setTracesFiltersParam(null);
|
||||
void setEventsFiltersParam(null);
|
||||
void logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleExplorePagesRedirect = (): void => {
|
||||
const urlQuery = new URLSearchParams();
|
||||
|
||||
if (selectedInterval !== 'custom') {
|
||||
urlQuery.set(QueryParams.relativeTime, selectedInterval);
|
||||
} else {
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
// Explorer URL params are in milliseconds, timeRange is in seconds
|
||||
urlQuery.set(QueryParams.startTime, (timeRange.startTime * 1000).toString());
|
||||
urlQuery.set(QueryParams.endTime, (timeRange.endTime * 1000).toString());
|
||||
}
|
||||
|
||||
void logEvent(InfraMonitoringEvents.ExploreClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const fullExpression = combineInitialAndUserExpression(
|
||||
logsAndTracesInitialExpression,
|
||||
userLogsExpression || '',
|
||||
);
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.logs,
|
||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||
expression: fullExpression,
|
||||
filter: { expression: fullExpression },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const fullExpression = combineInitialAndUserExpression(
|
||||
logsAndTracesInitialExpression,
|
||||
userTracesExpression || '',
|
||||
);
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
aggregateOperator: TracesAggregatorOperator.NOOP,
|
||||
expression: fullExpression,
|
||||
filter: { expression: fullExpression },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
{metadataConfig.map((config) => (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
color="muted"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
{config.label}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="values-row">
|
||||
{metadataConfig.map((config) => {
|
||||
const value = config.getValue(entity);
|
||||
const displayValue = String(value);
|
||||
return (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
className="entity-details-metadata-value"
|
||||
>
|
||||
{config.render ? (
|
||||
config.render(value, entity)
|
||||
) : (
|
||||
<Tooltip title={displayValue}>{displayValue}</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{countsConfig &&
|
||||
countsConfig.length > 0 &&
|
||||
selectedItem &&
|
||||
getCountsFilterExpression && (
|
||||
<EntityCountsSection
|
||||
entity={entity}
|
||||
countsConfig={countsConfig}
|
||||
selectedItem={selectedItem}
|
||||
filterExpression={getCountsFilterExpression(entity)}
|
||||
closeDrawer={handleClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!hideDetailViewTabs && (
|
||||
<div className="views-tabs-container">
|
||||
<ToggleGroupSimple
|
||||
type="single"
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
items={[
|
||||
...(tabVisibility.showMetrics
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.METRICS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<BarChart size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showLogs
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.LOGS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showTraces
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.TRACES,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<DraftingCompass size={14} />
|
||||
Traces
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showEvents
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.EVENTS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Events
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(customTabs?.map((tab) => ({
|
||||
value: tab.key,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</div>
|
||||
),
|
||||
})) ?? []),
|
||||
]}
|
||||
/>
|
||||
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<Tooltip title="Go to Logs Explorer" placement="left">
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<Tooltip title="Go to Traces Explorer" placement="left">
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{effectiveView === VIEW_TYPES.METRICS && (
|
||||
<EntityMetrics<T>
|
||||
entity={entity}
|
||||
eventEntity={eventCategory}
|
||||
entityWidgetInfo={entityWidgetInfo}
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
category={category}
|
||||
queryKey={`${queryKeyPrefix}Metrics`}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.LOGS && (
|
||||
<EntityLogs
|
||||
eventEntity={eventCategory}
|
||||
queryKey={`${queryKeyPrefix}Logs`}
|
||||
category={category}
|
||||
initialExpression={logsAndTracesInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.TRACES && (
|
||||
<EntityTraces
|
||||
eventEntity={eventCategory}
|
||||
queryKey={`${queryKeyPrefix}Traces`}
|
||||
category={category}
|
||||
initialExpression={logsAndTracesInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.EVENTS && tabVisibility.showEvents && (
|
||||
<EntityEvents
|
||||
eventEntity={eventCategory}
|
||||
queryKey={`${queryKeyPrefix}Events`}
|
||||
initialExpression={eventsInitialExpression}
|
||||
category={category}
|
||||
/>
|
||||
)}
|
||||
{customTabs?.map((tab) =>
|
||||
selectedView === tab.key ? (
|
||||
<Fragment key={tab.key}>
|
||||
{tab.render({
|
||||
entity,
|
||||
timeRange,
|
||||
selectedInterval,
|
||||
handleTimeChange,
|
||||
})}
|
||||
</Fragment>
|
||||
) : null,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -74,15 +74,14 @@ export type K8sBaseListProps<
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO | null;
|
||||
}>;
|
||||
/** Function to get the unique key for a row. */
|
||||
getRowKey: (record: T) => string;
|
||||
getRowKey?: (record: T) => string;
|
||||
/** Function to get the item key used for selection. Can return string or SelectedItemParams. */
|
||||
getItemKey: (record: T) => TItemKey;
|
||||
getItemKey?: (record: T) => TItemKey;
|
||||
eventCategory: InfraMonitoringEvents;
|
||||
renderEmptyState?: (
|
||||
context: K8sBaseListEmptyStateContext,
|
||||
) => React.ReactNode | null;
|
||||
extraQueryKeyParts?: string[];
|
||||
detailsQueryKeyPrefix: string;
|
||||
};
|
||||
|
||||
export function K8sBaseList<
|
||||
@@ -99,7 +98,6 @@ export function K8sBaseList<
|
||||
eventCategory,
|
||||
renderEmptyState,
|
||||
extraQueryKeyParts = [],
|
||||
detailsQueryKeyPrefix,
|
||||
}: K8sBaseListProps<T, TItemKey>): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const expression = currentQuery.builder.queryData[0]?.filter?.expression || '';
|
||||
@@ -236,29 +234,17 @@ export function K8sBaseList<
|
||||
}, [eventCategory, totalCount]);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(record: T, itemKey: TItemKey): void => {
|
||||
(_record: T, itemKey: TItemKey): void => {
|
||||
if (groupBy.length === 0) {
|
||||
const params: SelectedItemParams =
|
||||
typeof itemKey === 'object'
|
||||
? itemKey
|
||||
: {
|
||||
selectedItem: itemKey,
|
||||
clusterName: null,
|
||||
namespaceName: null,
|
||||
};
|
||||
|
||||
if (detailsQueryKeyPrefix) {
|
||||
const detailQueryKey = getAutoRefreshQueryKey(
|
||||
selectedTime,
|
||||
`${detailsQueryKeyPrefix}EntityDetails`,
|
||||
params.selectedItem,
|
||||
params.clusterName,
|
||||
params.namespaceName,
|
||||
);
|
||||
queryClient.setQueryData(detailQueryKey, { data: record });
|
||||
if (typeof itemKey === 'object' && itemKey !== null) {
|
||||
setSelectedItemParams(itemKey);
|
||||
} else {
|
||||
setSelectedItemParams({
|
||||
selectedItem: itemKey,
|
||||
clusterName: null,
|
||||
namespaceName: null,
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedItemParams(params);
|
||||
}
|
||||
|
||||
void logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
@@ -267,15 +253,7 @@ export function K8sBaseList<
|
||||
category: eventCategory,
|
||||
});
|
||||
},
|
||||
[
|
||||
eventCategory,
|
||||
groupBy.length,
|
||||
setSelectedItemParams,
|
||||
detailsQueryKeyPrefix,
|
||||
getAutoRefreshQueryKey,
|
||||
selectedTime,
|
||||
queryClient,
|
||||
],
|
||||
[eventCategory, groupBy.length, setSelectedItemParams],
|
||||
);
|
||||
|
||||
const handleRowClickNewTab = useCallback(
|
||||
@@ -425,7 +403,7 @@ export function K8sBaseList<
|
||||
onRowClickNewTab={handleRowClickNewTab}
|
||||
renderExpandedRow={isGroupedByAttribute ? renderExpandedRow : undefined}
|
||||
getRowCanExpand={isGroupedByAttribute ? getRowCanExpand : undefined}
|
||||
className={cx(styles.k8SListTable)}
|
||||
className={cx(styles.k8SListTable, expandedRowColumns)}
|
||||
enableQueryParams={{
|
||||
page: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
|
||||
limit: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useInfraMonitoringCategory } from '../hooks';
|
||||
import { getEntityConfig } from './entity.registry';
|
||||
import { K8sBaseList } from './K8sBaseList';
|
||||
import K8sBaseDetails from './K8sBaseDetails';
|
||||
|
||||
export interface K8sDynamicListProps {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
leftFilters?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function K8sDynamicList({
|
||||
controlListPrefix,
|
||||
leftFilters,
|
||||
}: K8sDynamicListProps): JSX.Element | null {
|
||||
const [selectedCategory] = useInfraMonitoringCategory();
|
||||
|
||||
const config = useMemo(
|
||||
() => getEntityConfig(selectedCategory),
|
||||
[selectedCategory],
|
||||
);
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { list, details } = config;
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList
|
||||
{...list}
|
||||
controlListPrefix={controlListPrefix}
|
||||
leftFilters={leftFilters}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails {...details} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sDynamicList;
|
||||
@@ -245,7 +245,6 @@ function K8sHeader<TData>({
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
defaultRelativeTime="30m"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Box } from '@signozhq/icons';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { act, render, waitFor } from 'tests/test-utils';
|
||||
@@ -32,7 +31,7 @@ const mockEntity: TestEntity = {
|
||||
function createBaseProps() {
|
||||
return {
|
||||
category: InfraMonitoringEntity.PODS,
|
||||
eventCategory: InfraMonitoringEvents.Pod,
|
||||
eventCategory: 'Pod',
|
||||
getSelectedItemExpression: (): string => 'k8s.pod.name = "test-pod"',
|
||||
fetchEntityData: jest
|
||||
.fn()
|
||||
|
||||
@@ -175,10 +175,8 @@ function renderComponent<
|
||||
>({
|
||||
queryParams,
|
||||
onUrlUpdate,
|
||||
detailsQueryKeyPrefix = 'testEntity',
|
||||
...props
|
||||
}: Omit<K8sBaseListProps<T, TItemKey>, 'detailsQueryKeyPrefix'> & {
|
||||
detailsQueryKeyPrefix?: string;
|
||||
}: K8sBaseListProps<T, TItemKey> & {
|
||||
queryParams?: Record<string, string>;
|
||||
onUrlUpdate?: OnUrlUpdateFunction;
|
||||
}) {
|
||||
@@ -205,10 +203,7 @@ function renderComponent<
|
||||
value={{ viewportHeight: 800, itemHeight: 50 }}
|
||||
>
|
||||
<TooltipProvider>
|
||||
<K8sBaseList<T, TItemKey>
|
||||
{...props}
|
||||
detailsQueryKeyPrefix={detailsQueryKeyPrefix}
|
||||
/>
|
||||
<K8sBaseList<T, TItemKey> {...props} />
|
||||
</TooltipProvider>
|
||||
</VirtuosoMockContext.Provider>
|
||||
</NuqsTestingAdapter>
|
||||
|
||||
@@ -52,6 +52,7 @@ export function EntityCountsSection<T>({
|
||||
},
|
||||
};
|
||||
|
||||
// TODO(H4ad): After https://github.com/SigNoz/signoz/pull/12038, inherit custom time of drawer to list
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY, targetCategory);
|
||||
urlParams.set(
|
||||
@@ -59,44 +60,6 @@ export function EntityCountsSection<T>({
|
||||
encodeURIComponent(JSON.stringify(compositeQuery)),
|
||||
);
|
||||
|
||||
const currentSearchParams = new URLSearchParams(window.location.search);
|
||||
const detailRelativeTime = currentSearchParams.get(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_RELATIVE_TIME,
|
||||
);
|
||||
const detailStartTime = currentSearchParams.get(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_START_TIME,
|
||||
);
|
||||
const detailEndTime = currentSearchParams.get(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_END_TIME,
|
||||
);
|
||||
|
||||
const listRelativeTime = currentSearchParams.get(QueryParams.relativeTime);
|
||||
const listStartTime = currentSearchParams.get(QueryParams.startTime);
|
||||
const listEndTime = currentSearchParams.get(QueryParams.endTime);
|
||||
|
||||
if (listRelativeTime) {
|
||||
urlParams.set(QueryParams.relativeTime, listRelativeTime);
|
||||
} else if (listStartTime && listEndTime) {
|
||||
urlParams.set(QueryParams.startTime, listStartTime);
|
||||
urlParams.set(QueryParams.endTime, listEndTime);
|
||||
}
|
||||
|
||||
if (detailRelativeTime) {
|
||||
urlParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_RELATIVE_TIME,
|
||||
detailRelativeTime,
|
||||
);
|
||||
} else if (detailStartTime && detailEndTime) {
|
||||
urlParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_START_TIME,
|
||||
detailStartTime,
|
||||
);
|
||||
urlParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_END_TIME,
|
||||
detailEndTime,
|
||||
);
|
||||
}
|
||||
|
||||
return `${ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES}?${urlParams.toString()}`;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import { K8sBaseListProps } from './K8sBaseList';
|
||||
import { K8sBaseDetailsProps } from './types';
|
||||
|
||||
export type K8sEntityData = { meta?: Record<string, string> | null };
|
||||
|
||||
export type K8sEntityListConfig<
|
||||
T extends K8sEntityData,
|
||||
TItemKey extends string | SelectedItemParams = string,
|
||||
> = Omit<K8sBaseListProps<T, TItemKey>, 'controlListPrefix' | 'leftFilters'>;
|
||||
|
||||
export type K8sEntityDetailsConfig<T extends K8sEntityData> =
|
||||
K8sBaseDetailsProps<T>;
|
||||
|
||||
export interface K8sEntityConfig<
|
||||
T extends K8sEntityData,
|
||||
TItemKey extends string | SelectedItemParams = string,
|
||||
> {
|
||||
list: K8sEntityListConfig<T, TItemKey>;
|
||||
details: K8sEntityDetailsConfig<T>;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { K8sCategories } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import { K8sEntityConfig, K8sEntityData } from './entity.config.types';
|
||||
|
||||
import { podEntityConfig } from '../Pods/entity.config';
|
||||
import { nodeEntityConfig } from '../Nodes/entity.config';
|
||||
import { clusterEntityConfig } from '../Clusters/entity.config';
|
||||
import { deploymentEntityConfig } from '../Deployments/entity.config';
|
||||
import { namespaceEntityConfig } from '../Namespaces/entity.config';
|
||||
import { jobEntityConfig } from '../Jobs/entity.config';
|
||||
import { daemonSetEntityConfig } from '../DaemonSets/entity.config';
|
||||
import { statefulSetEntityConfig } from '../StatefulSets/entity.config';
|
||||
import { volumeEntityConfig } from '../Volumes/entity.config';
|
||||
|
||||
type AnyEntityConfig = K8sEntityConfig<
|
||||
K8sEntityData,
|
||||
string | SelectedItemParams
|
||||
>;
|
||||
|
||||
function registerConfig<
|
||||
T extends K8sEntityData,
|
||||
TItemKey extends string | SelectedItemParams,
|
||||
>(config: K8sEntityConfig<T, TItemKey>): AnyEntityConfig {
|
||||
return config as unknown as AnyEntityConfig;
|
||||
}
|
||||
|
||||
export const entityRegistry: Record<string, AnyEntityConfig> = {
|
||||
[K8sCategories.PODS]: registerConfig(podEntityConfig),
|
||||
[K8sCategories.NODES]: registerConfig(nodeEntityConfig),
|
||||
[K8sCategories.CLUSTERS]: registerConfig(clusterEntityConfig),
|
||||
[K8sCategories.DEPLOYMENTS]: registerConfig(deploymentEntityConfig),
|
||||
[K8sCategories.NAMESPACES]: registerConfig(namespaceEntityConfig),
|
||||
[K8sCategories.JOBS]: registerConfig(jobEntityConfig),
|
||||
[K8sCategories.DAEMONSETS]: registerConfig(daemonSetEntityConfig),
|
||||
[K8sCategories.STATEFULSETS]: registerConfig(statefulSetEntityConfig),
|
||||
[K8sCategories.VOLUMES]: registerConfig(volumeEntityConfig),
|
||||
};
|
||||
|
||||
export function getEntityConfig(category: string): AnyEntityConfig | undefined {
|
||||
return entityRegistry[category];
|
||||
}
|
||||
@@ -1,18 +1,3 @@
|
||||
import { ReactNode } from 'react';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { EntityCountConfig } from './components/EntityCountsSection/EntityCountsSection';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
|
||||
export type K8sDetailsCountConfig<T> = EntityCountConfig<T>;
|
||||
|
||||
export type K8sBaseFilters = {
|
||||
filter: {
|
||||
expression: string;
|
||||
@@ -48,100 +33,3 @@ export type K8sTableRowData<T> = T & {
|
||||
/** Metadata about which attributes were used for grouping */
|
||||
groupedByMeta?: Record<string, string>;
|
||||
};
|
||||
|
||||
export interface K8sDetailsMetadataConfig<T> {
|
||||
label: string;
|
||||
getValue: (entity: T) => string | number;
|
||||
render?: (value: string | number, entity: T) => ReactNode;
|
||||
}
|
||||
|
||||
export interface K8sDetailsFilters {
|
||||
filter: { expression: string };
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface K8sDetailsWidgetInfo {
|
||||
title: string;
|
||||
yAxisUnit: string;
|
||||
}
|
||||
|
||||
export type GetEntityQueryPayload<T> = (
|
||||
entity: T,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
) => GetQueryResultsProps[];
|
||||
|
||||
export interface K8sDetailsTabsConfig {
|
||||
showMetrics?: boolean;
|
||||
showLogs?: boolean;
|
||||
showTraces?: boolean;
|
||||
showEvents?: boolean;
|
||||
}
|
||||
|
||||
export interface K8sDetailsCustomTabRenderProps<T> {
|
||||
entity: T;
|
||||
/** Time range in seconds — see useEntityDetailsTime */
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: Time;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface K8sDetailsCustomTab<T> {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: ReactNode;
|
||||
render: (props: K8sDetailsCustomTabRenderProps<T>) => ReactNode;
|
||||
}
|
||||
|
||||
export interface K8sBaseDetailsProps<T> {
|
||||
category: InfraMonitoringEntity;
|
||||
eventCategory: InfraMonitoringEvents;
|
||||
// Data fetching configuration
|
||||
getSelectedItemExpression: (params: SelectedItemParams) => string;
|
||||
fetchEntityData: (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{ data: T | null; error?: APIError | null }>;
|
||||
// Entity configuration
|
||||
getEntityName: (entity: T) => string;
|
||||
getInitialLogTracesExpression: (entity: T) => string;
|
||||
getInitialEventsExpression: (entity: T) => string;
|
||||
metadataConfig: K8sDetailsMetadataConfig<T>[];
|
||||
countsConfig?: K8sDetailsCountConfig<T>[];
|
||||
getCountsFilterExpression?: (entity: T) => string;
|
||||
entityWidgetInfo: K8sDetailsWidgetInfo[];
|
||||
getEntityQueryPayload: GetEntityQueryPayload<T>;
|
||||
queryKeyPrefix: string;
|
||||
/** When true, only metrics are shown and the Metrics/Logs/Traces/Events tab bar is hidden. */
|
||||
hideDetailViewTabs?: boolean;
|
||||
tabsConfig?: K8sDetailsTabsConfig;
|
||||
customTabs?: K8sDetailsCustomTab<T>[];
|
||||
}
|
||||
|
||||
export interface K8sBaseDetailsContentProps<T> {
|
||||
entity: T;
|
||||
category: InfraMonitoringEntity;
|
||||
eventCategory: InfraMonitoringEvents;
|
||||
metadataConfig: K8sDetailsMetadataConfig<T>[];
|
||||
countsConfig?: K8sDetailsCountConfig<T>[];
|
||||
getCountsFilterExpression?: (entity: T) => string;
|
||||
selectedItem: string | null;
|
||||
handleClose: () => void;
|
||||
entityWidgetInfo: K8sDetailsWidgetInfo[];
|
||||
getEntityQueryPayload: GetEntityQueryPayload<T>;
|
||||
queryKeyPrefix: string;
|
||||
hideDetailViewTabs: boolean;
|
||||
tabsConfig?: K8sDetailsTabsConfig;
|
||||
customTabs?: K8sDetailsCustomTab<T>[];
|
||||
logsAndTracesInitialExpression: string;
|
||||
eventsInitialExpression: string;
|
||||
}
|
||||
|
||||
// Aliases for backward compatibility
|
||||
export type CustomTab<T> = K8sDetailsCustomTab<T>;
|
||||
export type CustomTabRenderProps<T> = K8sDetailsCustomTabRenderProps<T>;
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { useCallback } from 'react';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listClusters } from 'api/generated/services/inframonitoring';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
InframonitoringtypesClusterRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
clusterWidgetInfo,
|
||||
getClusterMetricsQueryPayload,
|
||||
k8sClusterDetailsCountsConfig,
|
||||
k8sClusterDetailsMetadataConfig,
|
||||
k8sClusterGetCountsFilterExpression,
|
||||
k8sClusterGetEntityName,
|
||||
k8sClusterGetSelectedItemExpression,
|
||||
k8sClusterInitialEventsExpression,
|
||||
k8sClusterInitialLogTracesExpression,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sClusterItemKey,
|
||||
getK8sClusterRowKey,
|
||||
k8sClustersColumnsConfig,
|
||||
} from './table.config';
|
||||
|
||||
function K8sClustersList({
|
||||
controlListPrefix,
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
try {
|
||||
const response = await listClusters(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesClusterRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{
|
||||
data: InframonitoringtypesClusterRecordDTO | null;
|
||||
error?: APIError | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listClusters(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesClusterRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.CLUSTERS}
|
||||
tableColumns={k8sClustersColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
getRowKey={getK8sClusterRowKey}
|
||||
getItemKey={getK8sClusterItemKey}
|
||||
eventCategory={InfraMonitoringEvents.Cluster}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<InframonitoringtypesClusterRecordDTO>
|
||||
category={InfraMonitoringEntity.CLUSTERS}
|
||||
eventCategory={InfraMonitoringEvents.Cluster}
|
||||
getSelectedItemExpression={k8sClusterGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sClusterGetEntityName}
|
||||
getInitialLogTracesExpression={k8sClusterInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sClusterInitialEventsExpression}
|
||||
metadataConfig={k8sClusterDetailsMetadataConfig}
|
||||
countsConfig={k8sClusterDetailsCountsConfig}
|
||||
getCountsFilterExpression={k8sClusterGetCountsFilterExpression}
|
||||
entityWidgetInfo={clusterWidgetInfo}
|
||||
getEntityQueryPayload={getClusterMetricsQueryPayload}
|
||||
queryKeyPrefix="cluster"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sClustersList;
|
||||
@@ -1,138 +0,0 @@
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listClusters } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesClusterRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
|
||||
import { K8sEntityConfig } from '../Base/entity.config.types';
|
||||
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
clusterWidgetInfo,
|
||||
getClusterMetricsQueryPayload,
|
||||
k8sClusterDetailsCountsConfig,
|
||||
k8sClusterDetailsMetadataConfig,
|
||||
k8sClusterGetCountsFilterExpression,
|
||||
k8sClusterGetEntityName,
|
||||
k8sClusterGetSelectedItemExpression,
|
||||
k8sClusterInitialEventsExpression,
|
||||
k8sClusterInitialLogTracesExpression,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sClusterItemKey,
|
||||
getK8sClusterRowKey,
|
||||
k8sClustersColumnsConfig,
|
||||
} from './table.config';
|
||||
|
||||
async function fetchListData(
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<InframonitoringtypesClusterRecordDTO>['list']['fetchListData']
|
||||
> {
|
||||
try {
|
||||
const response = await listClusters(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesClusterRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEntityData(
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<InframonitoringtypesClusterRecordDTO>['details']['fetchEntityData']
|
||||
> {
|
||||
try {
|
||||
const response = await listClusters(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const clusterEntityConfig: K8sEntityConfig<InframonitoringtypesClusterRecordDTO> =
|
||||
{
|
||||
list: {
|
||||
entity: InfraMonitoringEntity.CLUSTERS,
|
||||
eventCategory: InfraMonitoringEvents.Cluster,
|
||||
tableColumns: k8sClustersColumnsConfig,
|
||||
fetchListData,
|
||||
getRowKey: getK8sClusterRowKey,
|
||||
getItemKey: getK8sClusterItemKey,
|
||||
detailsQueryKeyPrefix: 'cluster',
|
||||
},
|
||||
details: {
|
||||
category: InfraMonitoringEntity.CLUSTERS,
|
||||
eventCategory: InfraMonitoringEvents.Cluster,
|
||||
queryKeyPrefix: 'cluster',
|
||||
getSelectedItemExpression: k8sClusterGetSelectedItemExpression,
|
||||
fetchEntityData,
|
||||
getEntityName: k8sClusterGetEntityName,
|
||||
getInitialLogTracesExpression: k8sClusterInitialLogTracesExpression,
|
||||
getInitialEventsExpression: k8sClusterInitialEventsExpression,
|
||||
metadataConfig: k8sClusterDetailsMetadataConfig,
|
||||
entityWidgetInfo: clusterWidgetInfo,
|
||||
getEntityQueryPayload: getClusterMetricsQueryPayload,
|
||||
countsConfig: k8sClusterDetailsCountsConfig,
|
||||
getCountsFilterExpression: k8sClusterGetCountsFilterExpression,
|
||||
},
|
||||
};
|
||||
@@ -93,14 +93,13 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
|
||||
row.nodeCountsByReadiness,
|
||||
width: { min: 180 },
|
||||
enableSort: false,
|
||||
cell: ({ row, rowId }): React.ReactNode => {
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
if (!row.nodeCountsByReadiness) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
rowId={rowId}
|
||||
items={[
|
||||
{
|
||||
value: row.nodeCountsByReadiness.ready,
|
||||
@@ -130,16 +129,13 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
|
||||
row.podCountsByStatus,
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
cell: ({ row, rowId }): React.ReactNode => {
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByStatus = row.podCountsByStatus;
|
||||
if (!podCountsByStatus) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
rowId={rowId}
|
||||
items={getPodStatusItems(row.podCountsByStatus)}
|
||||
/>
|
||||
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listDaemonSets } from 'api/generated/services/inframonitoring';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
InframonitoringtypesDaemonSetRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
daemonSetWidgetInfo,
|
||||
getDaemonSetMetricsQueryPayload,
|
||||
getDaemonSetPodMetricsQueryPayload,
|
||||
k8sDaemonSetDetailsMetadataConfig,
|
||||
k8sDaemonSetGetEntityName,
|
||||
k8sDaemonSetGetSelectedItemExpression,
|
||||
k8sDaemonSetInitialEventsExpression,
|
||||
k8sDaemonSetInitialLogTracesExpression,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sDaemonSetItemKey,
|
||||
getK8sDaemonSetRowKey,
|
||||
k8sDaemonSetsColumnsConfig,
|
||||
} from './table.config';
|
||||
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
|
||||
|
||||
function K8sDaemonSetsList({
|
||||
controlListPrefix,
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
try {
|
||||
const response = await listDaemonSets(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesDaemonSetRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{
|
||||
data: InframonitoringtypesDaemonSetRecordDTO | null;
|
||||
error?: APIError | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listDaemonSets(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
const customTabs = useMemo(
|
||||
() => [
|
||||
createPodMetricsTab<InframonitoringtypesDaemonSetRecordDTO>({
|
||||
getQueryPayload: getDaemonSetPodMetricsQueryPayload,
|
||||
category: InfraMonitoringEntity.DAEMONSETS,
|
||||
queryKey: 'daemonSetPodMetrics',
|
||||
}),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesDaemonSetRecordDTO, SelectedItemParams>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
tableColumns={k8sDaemonSetsColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
getRowKey={getK8sDaemonSetRowKey}
|
||||
getItemKey={getK8sDaemonSetItemKey}
|
||||
eventCategory={InfraMonitoringEvents.DaemonSet}
|
||||
/>
|
||||
<K8sBaseDetails<InframonitoringtypesDaemonSetRecordDTO>
|
||||
category={InfraMonitoringEntity.DAEMONSETS}
|
||||
eventCategory={InfraMonitoringEvents.DaemonSet}
|
||||
getSelectedItemExpression={k8sDaemonSetGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sDaemonSetGetEntityName}
|
||||
getInitialLogTracesExpression={k8sDaemonSetInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sDaemonSetInitialEventsExpression}
|
||||
metadataConfig={k8sDaemonSetDetailsMetadataConfig}
|
||||
entityWidgetInfo={daemonSetWidgetInfo}
|
||||
getEntityQueryPayload={getDaemonSetMetricsQueryPayload}
|
||||
queryKeyPrefix="daemonset"
|
||||
customTabs={customTabs}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default K8sDaemonSetsList;
|
||||
@@ -1,153 +0,0 @@
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listDaemonSets } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesDaemonSetRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
|
||||
import { K8sEntityConfig } from '../Base/entity.config.types';
|
||||
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { createPodMetricsTab } from '../EntityDetailsUtils/createPodMetricsTab';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
daemonSetWidgetInfo,
|
||||
getDaemonSetMetricsQueryPayload,
|
||||
getDaemonSetPodMetricsQueryPayload,
|
||||
k8sDaemonSetDetailsMetadataConfig,
|
||||
k8sDaemonSetGetEntityName,
|
||||
k8sDaemonSetGetSelectedItemExpression,
|
||||
k8sDaemonSetInitialEventsExpression,
|
||||
k8sDaemonSetInitialLogTracesExpression,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sDaemonSetItemKey,
|
||||
getK8sDaemonSetRowKey,
|
||||
k8sDaemonSetsColumnsConfig,
|
||||
} from './table.config';
|
||||
|
||||
async function fetchListData(
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<
|
||||
InframonitoringtypesDaemonSetRecordDTO,
|
||||
SelectedItemParams
|
||||
>['list']['fetchListData']
|
||||
> {
|
||||
try {
|
||||
const response = await listDaemonSets(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesDaemonSetRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEntityData(
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<
|
||||
InframonitoringtypesDaemonSetRecordDTO,
|
||||
SelectedItemParams
|
||||
>['details']['fetchEntityData']
|
||||
> {
|
||||
try {
|
||||
const response = await listDaemonSets(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const daemonSetEntityConfig: K8sEntityConfig<
|
||||
InframonitoringtypesDaemonSetRecordDTO,
|
||||
SelectedItemParams
|
||||
> = {
|
||||
list: {
|
||||
entity: InfraMonitoringEntity.DAEMONSETS,
|
||||
eventCategory: InfraMonitoringEvents.DaemonSet,
|
||||
tableColumns: k8sDaemonSetsColumnsConfig,
|
||||
fetchListData,
|
||||
getRowKey: getK8sDaemonSetRowKey,
|
||||
getItemKey: getK8sDaemonSetItemKey,
|
||||
detailsQueryKeyPrefix: 'daemonset',
|
||||
},
|
||||
details: {
|
||||
category: InfraMonitoringEntity.DAEMONSETS,
|
||||
eventCategory: InfraMonitoringEvents.DaemonSet,
|
||||
queryKeyPrefix: 'daemonset',
|
||||
getSelectedItemExpression: k8sDaemonSetGetSelectedItemExpression,
|
||||
fetchEntityData,
|
||||
getEntityName: k8sDaemonSetGetEntityName,
|
||||
getInitialLogTracesExpression: k8sDaemonSetInitialLogTracesExpression,
|
||||
getInitialEventsExpression: k8sDaemonSetInitialEventsExpression,
|
||||
metadataConfig: k8sDaemonSetDetailsMetadataConfig,
|
||||
entityWidgetInfo: daemonSetWidgetInfo,
|
||||
getEntityQueryPayload: getDaemonSetMetricsQueryPayload,
|
||||
customTabs: [
|
||||
createPodMetricsTab<InframonitoringtypesDaemonSetRecordDTO>({
|
||||
getQueryPayload: getDaemonSetPodMetricsQueryPayload,
|
||||
category: InfraMonitoringEntity.DAEMONSETS,
|
||||
queryKey: 'daemonSetPodMetrics',
|
||||
docBasePath: '/infrastructure-monitoring/kubernetes/daemonsets/',
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -121,17 +121,12 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ row, rowId }): React.ReactNode => {
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByStatus = row.podCountsByStatus;
|
||||
if (!podCountsByStatus) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
rowId={rowId}
|
||||
items={getPodStatusItems(podCountsByStatus)}
|
||||
/>
|
||||
);
|
||||
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -145,9 +140,8 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
width: { min: 210 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ row, rowId }): React.ReactNode => (
|
||||
cell: ({ row }): React.ReactNode => (
|
||||
<GroupedStatusCounts
|
||||
rowId={rowId}
|
||||
items={[
|
||||
{
|
||||
value: row.readyNodes,
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listDeployments } from 'api/generated/services/inframonitoring';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
InframonitoringtypesDeploymentRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
deploymentWidgetInfo,
|
||||
getDeploymentMetricsQueryPayload,
|
||||
getDeploymentPodMetricsQueryPayload,
|
||||
k8sDeploymentDetailsMetadataConfig,
|
||||
k8sDeploymentGetEntityName,
|
||||
k8sDeploymentGetSelectedItemExpression,
|
||||
k8sDeploymentInitialEventsExpression,
|
||||
k8sDeploymentInitialLogTracesExpression,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sDeploymentItemKey,
|
||||
getK8sDeploymentRowKey,
|
||||
k8sDeploymentsColumnsConfig,
|
||||
} from './table.config';
|
||||
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
|
||||
|
||||
function K8sDeploymentsList({
|
||||
controlListPrefix,
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
try {
|
||||
const response = await listDeployments(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesDeploymentRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{
|
||||
data: InframonitoringtypesDeploymentRecordDTO | null;
|
||||
error?: APIError | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listDeployments(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const customTabs = useMemo(
|
||||
() => [
|
||||
createPodMetricsTab<InframonitoringtypesDeploymentRecordDTO>({
|
||||
getQueryPayload: getDeploymentPodMetricsQueryPayload,
|
||||
category: InfraMonitoringEntity.DEPLOYMENTS,
|
||||
queryKey: 'deploymentPodMetrics',
|
||||
}),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesDeploymentRecordDTO, SelectedItemParams>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
tableColumns={k8sDeploymentsColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
getRowKey={getK8sDeploymentRowKey}
|
||||
getItemKey={getK8sDeploymentItemKey}
|
||||
eventCategory={InfraMonitoringEvents.Deployment}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<InframonitoringtypesDeploymentRecordDTO>
|
||||
category={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
eventCategory={InfraMonitoringEvents.Deployment}
|
||||
getSelectedItemExpression={k8sDeploymentGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sDeploymentGetEntityName}
|
||||
getInitialLogTracesExpression={k8sDeploymentInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sDeploymentInitialEventsExpression}
|
||||
metadataConfig={k8sDeploymentDetailsMetadataConfig}
|
||||
entityWidgetInfo={deploymentWidgetInfo}
|
||||
getEntityQueryPayload={getDeploymentMetricsQueryPayload}
|
||||
queryKeyPrefix="deployment"
|
||||
customTabs={customTabs}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sDeploymentsList;
|
||||
@@ -1,161 +0,0 @@
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listDeployments } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesDeploymentRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
|
||||
import { K8sEntityConfig } from '../Base/entity.config.types';
|
||||
import {
|
||||
K8sBaseFilters,
|
||||
K8sDetailsCustomTab,
|
||||
K8sDetailsFilters,
|
||||
} from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { createPodMetricsTab } from '../EntityDetailsUtils/createPodMetricsTab';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
deploymentWidgetInfo,
|
||||
getDeploymentMetricsQueryPayload,
|
||||
getDeploymentPodMetricsQueryPayload,
|
||||
k8sDeploymentDetailsMetadataConfig,
|
||||
k8sDeploymentGetEntityName,
|
||||
k8sDeploymentGetSelectedItemExpression,
|
||||
k8sDeploymentInitialEventsExpression,
|
||||
k8sDeploymentInitialLogTracesExpression,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sDeploymentItemKey,
|
||||
getK8sDeploymentRowKey,
|
||||
k8sDeploymentsColumnsConfig,
|
||||
} from './table.config';
|
||||
|
||||
async function fetchListData(
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<
|
||||
InframonitoringtypesDeploymentRecordDTO,
|
||||
SelectedItemParams
|
||||
>['list']['fetchListData']
|
||||
> {
|
||||
try {
|
||||
const response = await listDeployments(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesDeploymentRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEntityData(
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<
|
||||
InframonitoringtypesDeploymentRecordDTO,
|
||||
SelectedItemParams
|
||||
>['details']['fetchEntityData']
|
||||
> {
|
||||
try {
|
||||
const response = await listDeployments(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function createCustomTabs(): K8sDetailsCustomTab<InframonitoringtypesDeploymentRecordDTO>[] {
|
||||
return [
|
||||
createPodMetricsTab<InframonitoringtypesDeploymentRecordDTO>({
|
||||
getQueryPayload: getDeploymentPodMetricsQueryPayload,
|
||||
category: InfraMonitoringEntity.DEPLOYMENTS,
|
||||
queryKey: 'deploymentPodMetrics',
|
||||
docBasePath: '/infrastructure-monitoring/kubernetes/deployments/',
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
export const deploymentEntityConfig: K8sEntityConfig<
|
||||
InframonitoringtypesDeploymentRecordDTO,
|
||||
SelectedItemParams
|
||||
> = {
|
||||
list: {
|
||||
entity: InfraMonitoringEntity.DEPLOYMENTS,
|
||||
eventCategory: InfraMonitoringEvents.Deployment,
|
||||
tableColumns: k8sDeploymentsColumnsConfig,
|
||||
fetchListData,
|
||||
getRowKey: getK8sDeploymentRowKey,
|
||||
getItemKey: getK8sDeploymentItemKey,
|
||||
detailsQueryKeyPrefix: 'deployment',
|
||||
},
|
||||
details: {
|
||||
category: InfraMonitoringEntity.DEPLOYMENTS,
|
||||
eventCategory: InfraMonitoringEvents.Deployment,
|
||||
queryKeyPrefix: 'deployment',
|
||||
getSelectedItemExpression: k8sDeploymentGetSelectedItemExpression,
|
||||
fetchEntityData,
|
||||
getEntityName: k8sDeploymentGetEntityName,
|
||||
getInitialLogTracesExpression: k8sDeploymentInitialLogTracesExpression,
|
||||
getInitialEventsExpression: k8sDeploymentInitialEventsExpression,
|
||||
metadataConfig: k8sDeploymentDetailsMetadataConfig,
|
||||
entityWidgetInfo: deploymentWidgetInfo,
|
||||
getEntityQueryPayload: getDeploymentMetricsQueryPayload,
|
||||
customTabs: createCustomTabs(),
|
||||
},
|
||||
};
|
||||
@@ -118,17 +118,12 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ row, rowId }): React.ReactNode => {
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByStatus = row.podCountsByStatus;
|
||||
if (!podCountsByStatus) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
rowId={rowId}
|
||||
items={getPodStatusItems(podCountsByStatus)}
|
||||
/>
|
||||
);
|
||||
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -142,9 +137,8 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
width: { min: 180 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ row, rowId }): React.ReactNode => (
|
||||
cell: ({ row }): React.ReactNode => (
|
||||
<GroupedStatusCounts
|
||||
rowId={rowId}
|
||||
items={[
|
||||
{
|
||||
value: row.availablePods,
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Undo } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
|
||||
import { useEntityDetailsTime } from './useEntityDetailsTime';
|
||||
|
||||
interface EntityDateTimeSelectorProps {
|
||||
eventEntity: string;
|
||||
category: InfraMonitoringEntity;
|
||||
view: string;
|
||||
}
|
||||
|
||||
function EntityDateTimeSelector({
|
||||
eventEntity,
|
||||
category,
|
||||
view,
|
||||
}: EntityDateTimeSelectorProps): JSX.Element {
|
||||
const {
|
||||
timeRange,
|
||||
selectedInterval,
|
||||
handleTimeChange,
|
||||
handleResetToParentTime,
|
||||
hasTimeChanged,
|
||||
} = useEntityDetailsTime();
|
||||
|
||||
const onTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
void logEvent(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: eventEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category,
|
||||
view,
|
||||
interval,
|
||||
});
|
||||
handleTimeChange(interval, dateTimeRange);
|
||||
},
|
||||
[category, view, eventEntity, handleTimeChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="entity-date-time-selector">
|
||||
{hasTimeChanged && (
|
||||
<TooltipSimple title="Reset to list time" side="bottom">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={handleResetToParentTime}
|
||||
data-testid="reset-to-list-time-button"
|
||||
prefix={<Undo size={14} />}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection
|
||||
onTimeChange={onTimeChange}
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EntityDateTimeSelector;
|
||||
@@ -1,95 +0,0 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
isCustomTimeRange,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
useGlobalTime,
|
||||
} from 'store/globalTime';
|
||||
|
||||
export interface EntityDetailsTimeRange {
|
||||
/** Unix timestamp in seconds — the unit API payload builders expect. Multiply by 1000 for ms-based UI (modals, URL params). */
|
||||
startTime: number;
|
||||
/** Unix timestamp in seconds — the unit API payload builders expect. Multiply by 1000 for ms-based UI (modals, URL params). */
|
||||
endTime: number;
|
||||
}
|
||||
|
||||
export interface UseEntityDetailsTimeResult {
|
||||
/** Time range in seconds */
|
||||
timeRange: EntityDetailsTimeRange;
|
||||
selectedInterval: Time;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
handleResetToParentTime: () => boolean;
|
||||
canResetToParent: boolean;
|
||||
hasTimeChanged: boolean;
|
||||
}
|
||||
|
||||
export function useEntityDetailsTime(): UseEntityDetailsTimeResult {
|
||||
const selectedTime = useGlobalTime((s) => s.selectedTime);
|
||||
const lastComputedMinMax = useGlobalTime((s) => s.lastComputedMinMax);
|
||||
const setSelectedTime = useGlobalTime((s) => s.setSelectedTime);
|
||||
const handleResetToParentTime = useGlobalTime((s) => s.resetToParentTime);
|
||||
const parentStore = useGlobalTime((s) => s.parentStore);
|
||||
|
||||
const canResetToParent = !!parentStore;
|
||||
|
||||
const hasTimeChanged = useMemo(() => {
|
||||
if (!parentStore) {
|
||||
return false;
|
||||
}
|
||||
return selectedTime !== parentStore.getState().selectedTime;
|
||||
}, [parentStore, selectedTime]);
|
||||
|
||||
const timeRange = useMemo<EntityDetailsTimeRange>(
|
||||
() => ({
|
||||
startTime: Math.floor(
|
||||
lastComputedMinMax.minTime / NANO_SECOND_MULTIPLIER / 1000,
|
||||
),
|
||||
endTime: Math.floor(
|
||||
lastComputedMinMax.maxTime / NANO_SECOND_MULTIPLIER / 1000,
|
||||
),
|
||||
}),
|
||||
[lastComputedMinMax],
|
||||
);
|
||||
|
||||
const selectedInterval = useMemo<Time>(
|
||||
() =>
|
||||
isCustomTimeRange(selectedTime)
|
||||
? ('custom' as Time)
|
||||
: (selectedTime as Time),
|
||||
[selectedTime],
|
||||
);
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
// DateTimeSelector delivers milliseconds; the store keys custom
|
||||
// ranges by nanoseconds.
|
||||
setSelectedTime(
|
||||
createCustomTimeRange(
|
||||
dateTimeRange[0] * NANO_SECOND_MULTIPLIER,
|
||||
dateTimeRange[1] * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setSelectedTime(interval as Time);
|
||||
}
|
||||
},
|
||||
[setSelectedTime],
|
||||
);
|
||||
|
||||
return {
|
||||
timeRange,
|
||||
selectedInterval,
|
||||
handleTimeChange,
|
||||
handleResetToParentTime,
|
||||
canResetToParent,
|
||||
hasTimeChanged,
|
||||
};
|
||||
}
|
||||
@@ -21,14 +21,17 @@ import Controls from 'container/Controls';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import LoadingContainer from 'container/InfraMonitoringK8sV2/LoadingContainer';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
|
||||
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
|
||||
import EntityEmptyState from '../EntityEmptyState/EntityEmptyState';
|
||||
import EntityError from '../EntityError/EntityError';
|
||||
import { EventContents } from './EventsContent';
|
||||
@@ -50,7 +53,16 @@ interface EventDataType {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
eventEntity: string;
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
queryKey: string;
|
||||
category: InfraMonitoringEntity;
|
||||
initialExpression: string;
|
||||
@@ -65,11 +77,13 @@ const handleExpandRow = (record: EventDataType): JSX.Element => (
|
||||
);
|
||||
|
||||
function EntityEventsContent({
|
||||
eventEntity,
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
selectedInterval,
|
||||
queryKey,
|
||||
category,
|
||||
}: Omit<Props, 'initialExpression'>): JSX.Element {
|
||||
const { timeRange } = useEntityDetailsTime();
|
||||
const expression = useExpression();
|
||||
const inputExpression = useInputExpression();
|
||||
const userExpression = useUserExpression();
|
||||
@@ -117,8 +131,8 @@ function EntityEventsContent({
|
||||
if (validation.isValid) {
|
||||
querySearchOnRun(newUserExpression || '');
|
||||
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: eventEntity,
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category,
|
||||
view: InfraMonitoringEvents.EventsView,
|
||||
@@ -127,14 +141,7 @@ function EntityEventsContent({
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
[
|
||||
inputExpression,
|
||||
initialExpression,
|
||||
refetch,
|
||||
querySearchOnRun,
|
||||
category,
|
||||
eventEntity,
|
||||
],
|
||||
[inputExpression, initialExpression, refetch, querySearchOnRun, category],
|
||||
);
|
||||
|
||||
const queryData = useMemo(
|
||||
@@ -222,10 +229,16 @@ function EntityEventsContent({
|
||||
<div className={styles.container}>
|
||||
<div className={styles.filterContainer}>
|
||||
<div className={styles.filterContainerTime}>
|
||||
<EntityDateTimeSelector
|
||||
eventEntity={eventEntity}
|
||||
category={category}
|
||||
view={InfraMonitoringEvents.EventsView}
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
|
||||
<RunQueryBtn
|
||||
|
||||
@@ -29,25 +29,25 @@ function verifyEntityEventsV5Request({
|
||||
expect(orderKeys).toContain('timestamp');
|
||||
}
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
default: ({
|
||||
onTimeChange,
|
||||
}: {
|
||||
onTimeChange?: (interval: string, dateTimeRange?: [number, number]) => void;
|
||||
}): JSX.Element => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="mock-datetime-selection"
|
||||
onClick={(): void => {
|
||||
onTimeChange?.('5m');
|
||||
}}
|
||||
>
|
||||
Select Time
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
|
||||
useEntityDetailsTime: (): {
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: string;
|
||||
handleTimeChange: jest.Mock;
|
||||
} => ({
|
||||
timeRange: { startTime: 1, endTime: 2 },
|
||||
selectedInterval: '5m',
|
||||
handleTimeChange: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('EntityEvents', () => {
|
||||
let capturedQueryRangePayloads: QueryRangePayloadV5[] = [];
|
||||
|
||||
@@ -69,7 +69,10 @@ describe('EntityEvents', () => {
|
||||
searchParams={`${K8S_ENTITY_EVENTS_EXPRESSION_KEY}=k8s.pod.name+%3D+%22x%22`}
|
||||
>
|
||||
<EntityEvents
|
||||
eventEntity="test"
|
||||
timeRange={{ startTime: 1, endTime: 2 }}
|
||||
isModalTimeSelection={false}
|
||||
handleTimeChange={jest.fn()}
|
||||
selectedInterval="5m"
|
||||
queryKey="test"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
initialExpression='k8s.pod.name = "x"'
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
|
||||
import { mockQueryRangeV5WithEventsResponse } from '__tests__/query_range_v5.util';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
createGlobalTimeStore,
|
||||
GlobalTimeContext,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime';
|
||||
import { act, render, waitFor } from 'tests/test-utils';
|
||||
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
|
||||
import EntityEvents from '../EntityEvents';
|
||||
import { K8S_ENTITY_EVENTS_EXPRESSION_KEY } from '../hooks';
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
|
||||
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
|
||||
|
||||
describe('EntityEvents time range wiring', () => {
|
||||
let capturedPayloads: QueryRangePayloadV5[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
capturedPayloads = [];
|
||||
mockQueryRangeV5WithEventsResponse({
|
||||
onReceiveRequest: async (req) => {
|
||||
const body = (await req.json()) as QueryRangePayloadV5;
|
||||
capturedPayloads.push(body);
|
||||
return {};
|
||||
},
|
||||
});
|
||||
mockFieldsAPIsWithEmptyResponse();
|
||||
});
|
||||
|
||||
it('should query the V5 API with the global time range converted to milliseconds', async () => {
|
||||
const store = createGlobalTimeStore();
|
||||
store
|
||||
.getState()
|
||||
.setSelectedTime(
|
||||
createCustomTimeRange(
|
||||
START_MS * NANO_SECOND_MULTIPLIER,
|
||||
END_MS * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
|
||||
const expression = 'k8s.pod.name = "test-pod"';
|
||||
|
||||
act(() => {
|
||||
render(
|
||||
<GlobalTimeContext.Provider value={store}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={`${K8S_ENTITY_EVENTS_EXPRESSION_KEY}=${encodeURIComponent(
|
||||
expression,
|
||||
)}`}
|
||||
>
|
||||
<EntityEvents
|
||||
eventEntity="test"
|
||||
queryKey="test"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
initialExpression={expression}
|
||||
/>
|
||||
</NuqsTestingAdapter>
|
||||
</GlobalTimeContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads).toHaveLength(1);
|
||||
});
|
||||
|
||||
expect(capturedPayloads[0].start).toBe(START_MS);
|
||||
expect(capturedPayloads[0].end).toBe(END_MS);
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,11 @@ import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants'
|
||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog';
|
||||
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
|
||||
import useScrollToLog from 'hooks/logs/useScrollToLog';
|
||||
@@ -33,8 +38,6 @@ import { ILog } from 'types/api/logs/log';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
|
||||
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
|
||||
import EntityEmptyState from '../EntityEmptyState/EntityEmptyState';
|
||||
import EntityError from '../EntityError/EntityError';
|
||||
import { isKeyNotFoundError } from '../utils';
|
||||
@@ -44,18 +47,29 @@ import { getEntityLogsQueryPayload } from './utils';
|
||||
import styles from './EntityLogs.module.scss';
|
||||
|
||||
interface Props {
|
||||
eventEntity: string;
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
queryKey: string;
|
||||
category: InfraMonitoringEntity;
|
||||
initialExpression: string;
|
||||
}
|
||||
|
||||
function EntityLogsContent({
|
||||
eventEntity,
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
selectedInterval,
|
||||
queryKey,
|
||||
category,
|
||||
}: Omit<Props, 'initialExpression'>): JSX.Element {
|
||||
const { timeRange } = useEntityDetailsTime();
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||
|
||||
const expression = useExpression();
|
||||
@@ -120,7 +134,7 @@ function EntityLogsContent({
|
||||
if (validation.isValid) {
|
||||
querySearchOnRun(newUserExpression);
|
||||
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category,
|
||||
@@ -225,10 +239,16 @@ function EntityLogsContent({
|
||||
<div className={styles.container}>
|
||||
<div className={styles.filterContainer}>
|
||||
<div className={styles.filterContainerTime}>
|
||||
<EntityDateTimeSelector
|
||||
eventEntity={eventEntity}
|
||||
category={category}
|
||||
view={InfraMonitoringEvents.LogsView}
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
|
||||
<RunQueryBtn
|
||||
|
||||
@@ -50,25 +50,25 @@ jest.mock(
|
||||
},
|
||||
);
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
default: ({
|
||||
onTimeChange,
|
||||
}: {
|
||||
onTimeChange?: (interval: string, dateTimeRange?: [number, number]) => void;
|
||||
}): JSX.Element => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="mock-datetime-selection"
|
||||
onClick={(): void => {
|
||||
onTimeChange?.('5m');
|
||||
}}
|
||||
>
|
||||
Select Time
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
|
||||
useEntityDetailsTime: (): {
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: string;
|
||||
handleTimeChange: jest.Mock;
|
||||
} => ({
|
||||
timeRange: { startTime: 1, endTime: 2 },
|
||||
selectedInterval: '5m',
|
||||
handleTimeChange: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('EntityLogs', () => {
|
||||
let capturedQueryRangePayloads: QueryRangePayloadV5[] = [];
|
||||
const itemHeight = 100;
|
||||
@@ -94,7 +94,10 @@ describe('EntityLogs', () => {
|
||||
>
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 500, itemHeight }}>
|
||||
<EntityLogs
|
||||
eventEntity="test"
|
||||
timeRange={{ startTime: 1, endTime: 2 }}
|
||||
isModalTimeSelection={false}
|
||||
handleTimeChange={jest.fn()}
|
||||
selectedInterval="5m"
|
||||
queryKey="test"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
initialExpression='k8s.pod.name = "x"'
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
|
||||
import { mockQueryRangeV5WithLogsResponse } from '__tests__/query_range_v5.util';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
createGlobalTimeStore,
|
||||
GlobalTimeContext,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime';
|
||||
import { act, render, waitFor } from 'tests/test-utils';
|
||||
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
|
||||
import EntityLogs from '../EntityLogs';
|
||||
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../hooks';
|
||||
|
||||
jest.mock(
|
||||
'components/OverlayScrollbar/OverlayScrollbar',
|
||||
() =>
|
||||
function MockOverlayScrollbar({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
return <div>{children}</div>;
|
||||
},
|
||||
);
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
|
||||
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
|
||||
|
||||
describe('EntityLogs time range wiring', () => {
|
||||
let capturedPayloads: QueryRangePayloadV5[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
capturedPayloads = [];
|
||||
mockQueryRangeV5WithLogsResponse({
|
||||
onReceiveRequest: async (req) => {
|
||||
const body = (await req.json()) as QueryRangePayloadV5;
|
||||
capturedPayloads.push(body);
|
||||
return {};
|
||||
},
|
||||
});
|
||||
mockFieldsAPIsWithEmptyResponse();
|
||||
});
|
||||
|
||||
it('should query the V5 API with the global time range converted to milliseconds', async () => {
|
||||
const store = createGlobalTimeStore();
|
||||
store
|
||||
.getState()
|
||||
.setSelectedTime(
|
||||
createCustomTimeRange(
|
||||
START_MS * NANO_SECOND_MULTIPLIER,
|
||||
END_MS * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
|
||||
const expression = 'k8s.pod.name = "test-pod"';
|
||||
|
||||
act(() => {
|
||||
render(
|
||||
<GlobalTimeContext.Provider value={store}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={`${K8S_ENTITY_LOGS_EXPRESSION_KEY}=${encodeURIComponent(
|
||||
expression,
|
||||
)}`}
|
||||
>
|
||||
<EntityLogs
|
||||
eventEntity="test"
|
||||
queryKey="test"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
initialExpression={expression}
|
||||
/>
|
||||
</NuqsTestingAdapter>
|
||||
</GlobalTimeContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
expect(capturedPayloads[0].start).toBe(START_MS);
|
||||
expect(capturedPayloads[0].end).toBe(END_MS);
|
||||
});
|
||||
});
|
||||
@@ -2,11 +2,15 @@ import { useCallback, useMemo, useRef } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Skeleton } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
@@ -20,17 +24,25 @@ import { getMetricsExplorerUrl } from 'utils/explorerUtils';
|
||||
import { buildEntityMetricsChartConfig } from './configBuilder';
|
||||
import ChartHeader from './ChartHeader';
|
||||
|
||||
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
|
||||
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
|
||||
import { useEntityMetrics } from './hooks';
|
||||
import { isKeyNotFoundError } from '../utils';
|
||||
|
||||
import styles from './EntityMetrics.module.scss';
|
||||
import { MetricsTable } from './MetricsTable';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
|
||||
interface EntityMetricsProps<T> {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
entity: T;
|
||||
eventEntity: string;
|
||||
entityWidgetInfo: {
|
||||
title: string;
|
||||
yAxisUnit: string;
|
||||
@@ -47,16 +59,16 @@ interface EntityMetricsProps<T> {
|
||||
}
|
||||
|
||||
function EntityMetrics<T>({
|
||||
selectedInterval,
|
||||
entity,
|
||||
eventEntity,
|
||||
timeRange,
|
||||
handleTimeChange,
|
||||
isModalTimeSelection,
|
||||
entityWidgetInfo,
|
||||
getEntityQueryPayload,
|
||||
queryKey,
|
||||
category,
|
||||
}: EntityMetricsProps<T>): JSX.Element {
|
||||
const { timeRange, selectedInterval, handleTimeChange } =
|
||||
useEntityDetailsTime();
|
||||
|
||||
const { visibilities, setElement } = useMultiIntersectionObserver(
|
||||
entityWidgetInfo.length,
|
||||
{ threshold: 0.1 },
|
||||
@@ -79,8 +91,10 @@ function EntityMetrics<T>({
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number): void => {
|
||||
// UPlotConfigBuilder's setSelect hook already delivers milliseconds
|
||||
handleTimeChange('custom', [Math.trunc(start), Math.trunc(end)]);
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
handleTimeChange('custom', [startTimestamp, endTimestamp]);
|
||||
},
|
||||
[handleTimeChange],
|
||||
);
|
||||
@@ -174,10 +188,16 @@ function EntityMetrics<T>({
|
||||
return (
|
||||
<>
|
||||
<div className={styles.metricsHeader}>
|
||||
<EntityDateTimeSelector
|
||||
eventEntity={eventEntity}
|
||||
category={category}
|
||||
view={InfraMonitoringEvents.MetricsView}
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime={DEFAULT_TIME_RANGE}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.entityMetricsContainer}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
import uPlot from 'uplot';
|
||||
@@ -27,28 +28,13 @@ jest.mock('lib/uPlotV2/utils/dataUtils', () => ({
|
||||
hasSingleVisiblePoint: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="date-time-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockTimeRange = { startTime: 1705315200, endTime: 1705318800 };
|
||||
let mockSelectedInterval = '5m';
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
|
||||
useEntityDetailsTime: (): {
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: string;
|
||||
handleTimeChange: jest.Mock;
|
||||
} => ({
|
||||
timeRange: mockTimeRange,
|
||||
selectedInterval: mockSelectedInterval,
|
||||
handleTimeChange: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries',
|
||||
() => ({
|
||||
@@ -158,6 +144,13 @@ const mockGetEntityQueryPayload = jest.fn().mockReturnValue([
|
||||
},
|
||||
]);
|
||||
|
||||
const mockTimeRange = {
|
||||
startTime: 1705315200,
|
||||
endTime: 1705318800,
|
||||
};
|
||||
|
||||
const mockHandleTimeChange = jest.fn();
|
||||
|
||||
const mockQueries = [
|
||||
{
|
||||
data: {
|
||||
@@ -290,6 +283,10 @@ const mockEmptyQueries = [
|
||||
|
||||
const renderEntityMetrics = (overrides = {}): any => {
|
||||
const defaultProps = {
|
||||
timeRange: mockTimeRange,
|
||||
isModalTimeSelection: false,
|
||||
handleTimeChange: mockHandleTimeChange,
|
||||
selectedInterval: '5m' as Time,
|
||||
entity: mockEntity,
|
||||
entityWidgetInfo: mockEntityWidgetInfo,
|
||||
getEntityQueryPayload: mockGetEntityQueryPayload,
|
||||
@@ -301,8 +298,11 @@ const renderEntityMetrics = (overrides = {}): any => {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<EntityMetrics
|
||||
timeRange={defaultProps.timeRange}
|
||||
isModalTimeSelection={defaultProps.isModalTimeSelection}
|
||||
handleTimeChange={defaultProps.handleTimeChange}
|
||||
selectedInterval={defaultProps.selectedInterval}
|
||||
entity={defaultProps.entity}
|
||||
eventEntity="test"
|
||||
entityWidgetInfo={defaultProps.entityWidgetInfo}
|
||||
getEntityQueryPayload={defaultProps.getEntityQueryPayload}
|
||||
queryKey={defaultProps.queryKey}
|
||||
@@ -344,7 +344,6 @@ const mockQueryPayloads = [
|
||||
describe('EntityMetrics', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSelectedInterval = '5m';
|
||||
mockUseEntityMetrics.mockReturnValue({
|
||||
queries: mockQueries as any,
|
||||
chartData: mockChartData,
|
||||
@@ -455,8 +454,7 @@ describe('EntityMetrics', () => {
|
||||
});
|
||||
|
||||
it('builds metrics explorer link with relativeTime when a relative interval is selected', () => {
|
||||
mockSelectedInterval = '5m';
|
||||
renderEntityMetrics();
|
||||
renderEntityMetrics({ selectedInterval: '5m' as Time });
|
||||
const href = screen
|
||||
.getByTestId('open-metrics-explorer-0')
|
||||
.getAttribute('href');
|
||||
@@ -466,8 +464,7 @@ describe('EntityMetrics', () => {
|
||||
});
|
||||
|
||||
it('builds metrics explorer link with absolute time range in milliseconds for custom interval', () => {
|
||||
mockSelectedInterval = 'custom';
|
||||
renderEntityMetrics();
|
||||
renderEntityMetrics({ selectedInterval: 'custom' as Time });
|
||||
const href = screen
|
||||
.getByTestId('open-metrics-explorer-0')
|
||||
.getAttribute('href');
|
||||
@@ -481,6 +478,7 @@ describe('EntityMetrics', () => {
|
||||
expect(mockUseEntityMetrics).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
queryKey: 'test-query-key',
|
||||
timeRange: mockTimeRange,
|
||||
entity: mockEntity,
|
||||
category: InfraMonitoringEntity.PODS,
|
||||
}),
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
createGlobalTimeStore,
|
||||
GlobalTimeContext,
|
||||
GlobalTimeStoreApi,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime';
|
||||
import { act, render } from 'tests/test-utils';
|
||||
|
||||
import { buildEntityMetricsChartConfig } from '../configBuilder';
|
||||
import EntityMetrics from '../EntityMetrics';
|
||||
|
||||
jest.mock('../configBuilder', () => ({
|
||||
buildEntityMetricsChartConfig: jest.fn().mockReturnValue({
|
||||
getId: jest.fn().mockReturnValue('mock-id'),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="date-time-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockBuildChartConfig = buildEntityMetricsChartConfig as jest.Mock;
|
||||
|
||||
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
|
||||
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
|
||||
const START_SECONDS = 1705315200;
|
||||
const END_SECONDS = 1705318800;
|
||||
|
||||
const entity = { id: 'test-entity-1' };
|
||||
|
||||
// The jsdom IntersectionObserver polyfill never reports visibility, so the
|
||||
// queries stay disabled and no network request is issued.
|
||||
const queryPayload = {
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
} as unknown as GetQueryResultsProps;
|
||||
|
||||
function createStoreWithCustomRange(): GlobalTimeStoreApi {
|
||||
const store = createGlobalTimeStore();
|
||||
store
|
||||
.getState()
|
||||
.setSelectedTime(
|
||||
createCustomTimeRange(
|
||||
START_MS * NANO_SECOND_MULTIPLIER,
|
||||
END_MS * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
return store;
|
||||
}
|
||||
|
||||
function renderEntityMetrics(store: GlobalTimeStoreApi): jest.Mock {
|
||||
const getEntityQueryPayload = jest.fn().mockReturnValue([queryPayload]);
|
||||
render(
|
||||
<GlobalTimeContext.Provider value={store}>
|
||||
<EntityMetrics
|
||||
entity={entity}
|
||||
eventEntity="test"
|
||||
entityWidgetInfo={[{ title: 'CPU Usage', yAxisUnit: 'percentage' }]}
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
queryKey="test-query-key"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
/>
|
||||
</GlobalTimeContext.Provider>,
|
||||
);
|
||||
return getEntityQueryPayload;
|
||||
}
|
||||
|
||||
describe('EntityMetrics time range wiring', () => {
|
||||
beforeEach(() => {
|
||||
mockBuildChartConfig.mockClear();
|
||||
});
|
||||
|
||||
it('should build the metrics query payloads and chart config from the global time range in seconds', () => {
|
||||
const store = createStoreWithCustomRange();
|
||||
|
||||
const getEntityQueryPayload = renderEntityMetrics(store);
|
||||
|
||||
expect(getEntityQueryPayload).toHaveBeenCalledWith(
|
||||
entity,
|
||||
START_SECONDS,
|
||||
END_SECONDS,
|
||||
false,
|
||||
);
|
||||
expect(mockBuildChartConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
minTimeScale: START_SECONDS,
|
||||
maxTimeScale: END_SECONDS,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should store a drag selection (received in milliseconds) as the equivalent custom range', () => {
|
||||
const store = createStoreWithCustomRange();
|
||||
|
||||
renderEntityMetrics(store);
|
||||
|
||||
const { onDragSelect } = mockBuildChartConfig.mock.calls[0][0];
|
||||
|
||||
// UPlotConfigBuilder's setSelect hook calls onDragSelect with milliseconds
|
||||
const dragStartMs = 1705316000000;
|
||||
const dragEndMs = 1705317000000;
|
||||
act(() => {
|
||||
onDragSelect(dragStartMs, dragEndMs);
|
||||
});
|
||||
|
||||
expect(store.getState().selectedTime).toBe(
|
||||
createCustomTimeRange(
|
||||
dragStartMs * NANO_SECOND_MULTIPLIER,
|
||||
dragEndMs * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,11 @@ import { InfraMonitoringEvents } from 'constants/events';
|
||||
import Controls from 'container/Controls';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
|
||||
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
|
||||
import { useQueryState } from 'nuqs';
|
||||
@@ -27,11 +32,10 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
|
||||
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
|
||||
import EntityEmptyState from '../EntityEmptyState/EntityEmptyState';
|
||||
import EntityError from '../EntityError/EntityError';
|
||||
import { isKeyNotFoundError, selectedEntityTracesColumns } from '../utils';
|
||||
import { selectedEntityTracesColumns } from '../utils';
|
||||
import { isKeyNotFoundError } from '../utils';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY, useEntityTraces } from './hooks';
|
||||
import { getTraceListColumns } from './traceListColumns';
|
||||
import { getEntityTracesQueryPayload } from './utils';
|
||||
@@ -40,18 +44,29 @@ import styles from './EntityTraces.module.scss';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
interface Props {
|
||||
eventEntity: string;
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
queryKey: string;
|
||||
category: InfraMonitoringEntity;
|
||||
initialExpression: string;
|
||||
}
|
||||
|
||||
function EntityTracesContent({
|
||||
eventEntity,
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
selectedInterval,
|
||||
queryKey,
|
||||
category,
|
||||
}: Omit<Props, 'initialExpression'>): JSX.Element {
|
||||
const { timeRange } = useEntityDetailsTime();
|
||||
const expression = useExpression();
|
||||
const inputExpression = useInputExpression();
|
||||
const userExpression = useUserExpression();
|
||||
@@ -99,8 +114,8 @@ function EntityTracesContent({
|
||||
if (validation.isValid) {
|
||||
querySearchOnRun(newUserExpression || '');
|
||||
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: eventEntity,
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
@@ -109,14 +124,7 @@ function EntityTracesContent({
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
[
|
||||
inputExpression,
|
||||
initialExpression,
|
||||
refetch,
|
||||
querySearchOnRun,
|
||||
category,
|
||||
eventEntity,
|
||||
],
|
||||
[inputExpression, initialExpression, refetch, querySearchOnRun, category],
|
||||
);
|
||||
|
||||
const queryData = useMemo(
|
||||
@@ -144,7 +152,7 @@ function EntityTracesContent({
|
||||
const hasAdditionalFilters = !!userExpression?.trim();
|
||||
|
||||
const handleRowClick = useCallback(() => {
|
||||
void logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
category,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
@@ -161,10 +169,16 @@ function EntityTracesContent({
|
||||
<div className={styles.container}>
|
||||
<div className={styles.filterContainer}>
|
||||
<div className={styles.filterContainerTime}>
|
||||
<EntityDateTimeSelector
|
||||
eventEntity={eventEntity}
|
||||
category={category}
|
||||
view={InfraMonitoringEvents.TracesView}
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
|
||||
<RunQueryBtn
|
||||
|
||||
@@ -34,8 +34,10 @@ describe('EntityTraces - Default Behavior', () => {
|
||||
});
|
||||
|
||||
it('should pass time range to API (converted to milliseconds)', async () => {
|
||||
const timeRange = { startTime: 1000, endTime: 2000 };
|
||||
|
||||
act(() => {
|
||||
renderEntityTraces();
|
||||
renderEntityTraces({ timeRange });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -45,8 +47,8 @@ describe('EntityTraces - Default Behavior', () => {
|
||||
verifyQueryPayload({
|
||||
payload: capturedPayloads[0],
|
||||
expectedTimeRange: {
|
||||
start: 1 * 1000,
|
||||
end: 2 * 1000,
|
||||
start: timeRange.startTime * 1000,
|
||||
end: timeRange.endTime * 1000,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
|
||||
import { mockQueryRangeV5WithTracesResponse } from '__tests__/query_range_v5.util';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
createGlobalTimeStore,
|
||||
GlobalTimeContext,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime';
|
||||
import { act, render, waitFor } from 'tests/test-utils';
|
||||
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
|
||||
import EntityTraces from '../EntityTraces';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../hooks';
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
|
||||
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
|
||||
|
||||
describe('EntityTraces time range wiring', () => {
|
||||
let capturedPayloads: QueryRangePayloadV5[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
capturedPayloads = [];
|
||||
mockQueryRangeV5WithTracesResponse({
|
||||
onReceiveRequest: async (req) => {
|
||||
const body = (await req.json()) as QueryRangePayloadV5;
|
||||
capturedPayloads.push(body);
|
||||
return {};
|
||||
},
|
||||
});
|
||||
mockFieldsAPIsWithEmptyResponse();
|
||||
});
|
||||
|
||||
it('should query the V5 API with the global time range converted to milliseconds', async () => {
|
||||
const store = createGlobalTimeStore();
|
||||
store
|
||||
.getState()
|
||||
.setSelectedTime(
|
||||
createCustomTimeRange(
|
||||
START_MS * NANO_SECOND_MULTIPLIER,
|
||||
END_MS * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
|
||||
const expression = 'k8s.pod.name = "test-pod"';
|
||||
|
||||
act(() => {
|
||||
render(
|
||||
<GlobalTimeContext.Provider value={store}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={`${K8S_ENTITY_TRACES_EXPRESSION_KEY}=${encodeURIComponent(
|
||||
expression,
|
||||
)}`}
|
||||
>
|
||||
<EntityTraces
|
||||
eventEntity="test"
|
||||
queryKey="test"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
initialExpression={expression}
|
||||
/>
|
||||
</NuqsTestingAdapter>
|
||||
</GlobalTimeContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads).toHaveLength(1);
|
||||
});
|
||||
|
||||
expect(capturedPayloads[0].start).toBe(START_MS);
|
||||
expect(capturedPayloads[0].end).toBe(END_MS);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { render, RenderResult } from 'tests/test-utils';
|
||||
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
@@ -7,19 +9,39 @@ import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
import EntityTraces from '../EntityTraces';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../hooks';
|
||||
|
||||
// QuerySearch fires autocomplete requests on mount; without handlers MSW
|
||||
// passes them through to the real network and the resulting AxiosError fails
|
||||
// whichever test happens to be running.
|
||||
beforeEach(() => {
|
||||
mockFieldsAPIsWithEmptyResponse();
|
||||
server.use(
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: { keys: {}, complete: true } }),
|
||||
),
|
||||
),
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/values`, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: { values: {}, complete: true } }),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
export interface RenderEntityTracesOptions {
|
||||
expression?: string;
|
||||
timeRange?: { startTime: number; endTime: number };
|
||||
category?: InfraMonitoringEntity;
|
||||
selectedInterval?: '5m' | '15m' | '30m' | '1h';
|
||||
pagination?: { offset: number; limit: number };
|
||||
}
|
||||
|
||||
export function renderEntityTraces({
|
||||
expression = 'k8s.pod.name = "test-pod"',
|
||||
timeRange = { startTime: 1, endTime: 2 },
|
||||
category = InfraMonitoringEntity.PODS,
|
||||
selectedInterval = '5m',
|
||||
pagination,
|
||||
}: RenderEntityTracesOptions = {}): RenderResult {
|
||||
const encodedExpression = encodeURIComponent(expression);
|
||||
@@ -33,7 +55,10 @@ export function renderEntityTraces({
|
||||
return render(
|
||||
<NuqsTestingAdapter searchParams={searchParams}>
|
||||
<EntityTraces
|
||||
eventEntity="test"
|
||||
timeRange={timeRange}
|
||||
isModalTimeSelection={false}
|
||||
handleTimeChange={jest.fn()}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="test"
|
||||
category={category}
|
||||
initialExpression={expression}
|
||||
@@ -76,21 +101,21 @@ export function verifyQueryPayload({
|
||||
expect(orderKeys).toContain('timestamp');
|
||||
}
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
default: ({
|
||||
onTimeChange,
|
||||
}: {
|
||||
onTimeChange?: (interval: string, dateTimeRange?: [number, number]) => void;
|
||||
}): JSX.Element => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="mock-datetime-selection"
|
||||
onClick={(): void => {
|
||||
onTimeChange?.('5m');
|
||||
}}
|
||||
>
|
||||
Select Time
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
|
||||
useEntityDetailsTime: (): {
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: string;
|
||||
handleTimeChange: jest.Mock;
|
||||
} => ({
|
||||
timeRange: { startTime: 1, endTime: 2 },
|
||||
selectedInterval: '5m',
|
||||
handleTimeChange: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Container } from '@signozhq/icons';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
|
||||
import { CustomTab } from '../Base/K8sBaseDetails';
|
||||
@@ -11,20 +10,6 @@ import {
|
||||
|
||||
import EntityMetrics from './EntityMetrics';
|
||||
|
||||
const categoryToEventEntity: Record<InfraMonitoringEntity, string> = {
|
||||
[InfraMonitoringEntity.DAEMONSETS]: InfraMonitoringEvents.DaemonSet,
|
||||
[InfraMonitoringEntity.DEPLOYMENTS]: InfraMonitoringEvents.Deployment,
|
||||
[InfraMonitoringEntity.JOBS]: InfraMonitoringEvents.Job,
|
||||
[InfraMonitoringEntity.NAMESPACES]: InfraMonitoringEvents.Namespace,
|
||||
[InfraMonitoringEntity.STATEFULSETS]: InfraMonitoringEvents.StatefulSet,
|
||||
[InfraMonitoringEntity.PODS]: InfraMonitoringEvents.Pod,
|
||||
[InfraMonitoringEntity.NODES]: InfraMonitoringEvents.Node,
|
||||
[InfraMonitoringEntity.CLUSTERS]: InfraMonitoringEvents.Cluster,
|
||||
[InfraMonitoringEntity.VOLUMES]: InfraMonitoringEvents.Volume,
|
||||
[InfraMonitoringEntity.HOSTS]: InfraMonitoringEvents.HostEntity,
|
||||
[InfraMonitoringEntity.CONTAINERS]: 'container',
|
||||
};
|
||||
|
||||
interface CreatePodMetricsTabParams<T> {
|
||||
getQueryPayload: (
|
||||
entity: T,
|
||||
@@ -32,38 +17,30 @@ interface CreatePodMetricsTabParams<T> {
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
) => GetQueryResultsProps[];
|
||||
queryKey: string;
|
||||
category: InfraMonitoringEntity;
|
||||
docBasePath?: string;
|
||||
queryKey: string;
|
||||
}
|
||||
|
||||
export function createPodMetricsTab<T>({
|
||||
getQueryPayload,
|
||||
queryKey,
|
||||
category,
|
||||
docBasePath,
|
||||
queryKey,
|
||||
}: CreatePodMetricsTabParams<T>): CustomTab<T> {
|
||||
const eventEntity = categoryToEventEntity[category];
|
||||
|
||||
const widgetInfo = docBasePath
|
||||
? podUtilizationByPodWidgetInfo.map((widget) => ({
|
||||
...widget,
|
||||
docPath: widget.docPath ? `${docBasePath}${widget.docPath}` : undefined,
|
||||
}))
|
||||
: podUtilizationByPodWidgetInfo;
|
||||
|
||||
return {
|
||||
key: VIEW_TYPES.POD_METRICS,
|
||||
label: 'Pod Metrics',
|
||||
icon: <Container size={14} />,
|
||||
render: ({ entity }) => (
|
||||
render: ({ entity, timeRange, selectedInterval, handleTimeChange }) => (
|
||||
<EntityMetrics
|
||||
entity={entity}
|
||||
eventEntity={eventEntity}
|
||||
entityWidgetInfo={widgetInfo}
|
||||
selectedInterval={selectedInterval}
|
||||
timeRange={timeRange}
|
||||
handleTimeChange={handleTimeChange}
|
||||
isModalTimeSelection
|
||||
entityWidgetInfo={podUtilizationByPodWidgetInfo}
|
||||
getEntityQueryPayload={getQueryPayload}
|
||||
queryKey={queryKey}
|
||||
category={category}
|
||||
queryKey={queryKey}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -454,9 +454,3 @@
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.entity-date-time-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,46 @@
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
|
||||
}
|
||||
|
||||
:global(.quick-filters) {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quickFiltersContainerHeader {
|
||||
|
||||
@@ -28,7 +28,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { FeatureKeys } from '../../constants/features';
|
||||
import { useAppContext } from '../../providers/App/App';
|
||||
import { K8sDynamicList } from './Base/K8sDynamicList';
|
||||
import K8sClustersList from './Clusters/K8sClustersList';
|
||||
import {
|
||||
GetClustersQuickFiltersConfig,
|
||||
GetDaemonsetsQuickFiltersConfig,
|
||||
@@ -43,18 +43,25 @@ import {
|
||||
K8sCategories,
|
||||
METRIC_NAMESPACE_BY_ENTITY,
|
||||
} from './constants';
|
||||
import K8sDaemonSetsList from './DaemonSets/K8sDaemonSetsList';
|
||||
import K8sDeploymentsList from './Deployments/K8sDeploymentsList';
|
||||
import {
|
||||
useInfraMonitoringCategory,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringSelectedItemParams,
|
||||
} from './hooks';
|
||||
import K8sJobsList from './Jobs/K8sJobsList';
|
||||
import K8sNamespacesList from './Namespaces/K8sNamespacesList';
|
||||
import K8sNodesList from './Nodes/K8sNodesList';
|
||||
import K8sPodLists from './Pods/K8sPodLists';
|
||||
import K8sStatefulSetsList from './StatefulSets/K8sStatefulSetsList';
|
||||
import K8sVolumesList from './Volumes/K8sVolumesList';
|
||||
|
||||
import styles from './InfraMonitoringK8s.module.scss';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { NANO_SECOND_MULTIPLIER, useGlobalTimeStore } from 'store/globalTime';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
|
||||
export default function InfraMonitoringK8s(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
@@ -245,62 +252,58 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
<div className={styles.infraContentRow}>
|
||||
{showFilters && (
|
||||
<div className={styles.quickFiltersContainer}>
|
||||
<OverlayScrollbar>
|
||||
<>
|
||||
<div className={styles.categorySelectorSection}>
|
||||
<div className={styles.sectionHeader} data-type="resource">
|
||||
<Typography.Text className={styles.sectionLabel}>
|
||||
Viewing · Resource
|
||||
</Typography.Text>
|
||||
<div className={styles.sectionLine} />
|
||||
<Tooltip title="Collapse Filters">
|
||||
<ArrowUpToLine
|
||||
style={{ transform: 'rotate(270deg)' }}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
size="md"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className={styles.categoryCard}>
|
||||
<div className={styles.categoryList}>
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category.key}
|
||||
type="button"
|
||||
className={`${styles.categoryItem} ${
|
||||
selectedCategory === category.key
|
||||
? styles.categoryItemSelected
|
||||
: ''
|
||||
}`}
|
||||
onClick={(): void => handleCategorySelect(category.key)}
|
||||
data-testid={`category-${category.key}`}
|
||||
>
|
||||
{category.icon}
|
||||
<Typography.Text>{category.label}</Typography.Text>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.categorySelectorSection}>
|
||||
<div className={styles.sectionHeader} data-type="resource">
|
||||
<Typography.Text className={styles.sectionLabel}>
|
||||
Viewing · Resource
|
||||
</Typography.Text>
|
||||
<div className={styles.sectionLine} />
|
||||
<Tooltip title="Collapse Filters">
|
||||
<ArrowUpToLine
|
||||
style={{ transform: 'rotate(270deg)' }}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
size="md"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className={styles.categoryCard}>
|
||||
<div className={styles.categoryList}>
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category.key}
|
||||
type="button"
|
||||
className={`${styles.categoryItem} ${
|
||||
selectedCategory === category.key
|
||||
? styles.categoryItemSelected
|
||||
: ''
|
||||
}`}
|
||||
onClick={(): void => handleCategorySelect(category.key)}
|
||||
data-testid={`category-${category.key}`}
|
||||
>
|
||||
{category.icon}
|
||||
<Typography.Text>{category.label}</Typography.Text>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.quickFiltersSection}>
|
||||
<div className={styles.sectionHeader} data-type="filter">
|
||||
<Typography.Text className={styles.sectionLabel}>
|
||||
Filter by
|
||||
</Typography.Text>
|
||||
<div className={styles.sectionLine} />
|
||||
</div>
|
||||
{selectedCategoryConfig && (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={selectedCategoryConfig}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={selectedCategoryUseFieldApis}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</OverlayScrollbar>
|
||||
<div className={styles.quickFiltersSection}>
|
||||
<div className={styles.sectionHeader} data-type="filter">
|
||||
<Typography.Text className={styles.sectionLabel}>
|
||||
Filter by
|
||||
</Typography.Text>
|
||||
<div className={styles.sectionLine} />
|
||||
</div>
|
||||
{selectedCategoryConfig && (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={selectedCategoryConfig}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={selectedCategoryUseFieldApis}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -309,7 +312,41 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
showFilters ? styles.listContainerFiltersVisible : ''
|
||||
}`}
|
||||
>
|
||||
<K8sDynamicList controlListPrefix={showFiltersComp} />
|
||||
{selectedCategory === K8sCategories.PODS && (
|
||||
<K8sPodLists controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.NODES && (
|
||||
<K8sNodesList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.CLUSTERS && (
|
||||
<K8sClustersList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.DEPLOYMENTS && (
|
||||
<K8sDeploymentsList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.NAMESPACES && (
|
||||
<K8sNamespacesList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.STATEFULSETS && (
|
||||
<K8sStatefulSetsList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.JOBS && (
|
||||
<K8sJobsList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.DAEMONSETS && (
|
||||
<K8sDaemonSetsList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.VOLUMES && (
|
||||
<K8sVolumesList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
164
frontend/src/container/InfraMonitoringK8sV2/Jobs/K8sJobsList.tsx
Normal file
164
frontend/src/container/InfraMonitoringK8sV2/Jobs/K8sJobsList.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listJobs } from 'api/generated/services/inframonitoring';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
InframonitoringtypesJobRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
getJobMetricsQueryPayload,
|
||||
getJobPodMetricsQueryPayload,
|
||||
jobWidgetInfo,
|
||||
k8sJobDetailsMetadataConfig,
|
||||
k8sJobGetEntityName,
|
||||
k8sJobGetSelectedItemExpression,
|
||||
k8sJobInitialEventsExpression,
|
||||
k8sJobInitialLogTracesExpression,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sJobItemKey,
|
||||
getK8sJobRowKey,
|
||||
k8sJobsColumnsConfig,
|
||||
} from './table.config';
|
||||
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
|
||||
|
||||
function K8sJobsList({
|
||||
controlListPrefix,
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
try {
|
||||
const response = await listJobs(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesJobRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{
|
||||
data: InframonitoringtypesJobRecordDTO | null;
|
||||
error?: APIError | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listJobs(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const customTabs = useMemo(
|
||||
() => [
|
||||
createPodMetricsTab<InframonitoringtypesJobRecordDTO>({
|
||||
getQueryPayload: getJobPodMetricsQueryPayload,
|
||||
category: InfraMonitoringEntity.JOBS,
|
||||
queryKey: 'jobPodMetrics',
|
||||
}),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesJobRecordDTO, SelectedItemParams>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
tableColumns={k8sJobsColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
getRowKey={getK8sJobRowKey}
|
||||
getItemKey={getK8sJobItemKey}
|
||||
eventCategory={InfraMonitoringEvents.Job}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<InframonitoringtypesJobRecordDTO>
|
||||
category={InfraMonitoringEntity.JOBS}
|
||||
eventCategory={InfraMonitoringEvents.Job}
|
||||
getSelectedItemExpression={k8sJobGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sJobGetEntityName}
|
||||
getInitialLogTracesExpression={k8sJobInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sJobInitialEventsExpression}
|
||||
metadataConfig={k8sJobDetailsMetadataConfig}
|
||||
entityWidgetInfo={jobWidgetInfo}
|
||||
getEntityQueryPayload={getJobMetricsQueryPayload}
|
||||
queryKeyPrefix="job"
|
||||
customTabs={customTabs}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sJobsList;
|
||||
@@ -1,153 +0,0 @@
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listJobs } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesJobRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
|
||||
import { K8sEntityConfig } from '../Base/entity.config.types';
|
||||
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { createPodMetricsTab } from '../EntityDetailsUtils/createPodMetricsTab';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
getJobMetricsQueryPayload,
|
||||
getJobPodMetricsQueryPayload,
|
||||
jobWidgetInfo,
|
||||
k8sJobDetailsMetadataConfig,
|
||||
k8sJobGetEntityName,
|
||||
k8sJobGetSelectedItemExpression,
|
||||
k8sJobInitialEventsExpression,
|
||||
k8sJobInitialLogTracesExpression,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sJobItemKey,
|
||||
getK8sJobRowKey,
|
||||
k8sJobsColumnsConfig,
|
||||
} from './table.config';
|
||||
|
||||
async function fetchListData(
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<
|
||||
InframonitoringtypesJobRecordDTO,
|
||||
SelectedItemParams
|
||||
>['list']['fetchListData']
|
||||
> {
|
||||
try {
|
||||
const response = await listJobs(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesJobRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEntityData(
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<
|
||||
InframonitoringtypesJobRecordDTO,
|
||||
SelectedItemParams
|
||||
>['details']['fetchEntityData']
|
||||
> {
|
||||
try {
|
||||
const response = await listJobs(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const jobEntityConfig: K8sEntityConfig<
|
||||
InframonitoringtypesJobRecordDTO,
|
||||
SelectedItemParams
|
||||
> = {
|
||||
list: {
|
||||
entity: InfraMonitoringEntity.JOBS,
|
||||
eventCategory: InfraMonitoringEvents.Job,
|
||||
tableColumns: k8sJobsColumnsConfig,
|
||||
fetchListData,
|
||||
getRowKey: getK8sJobRowKey,
|
||||
getItemKey: getK8sJobItemKey,
|
||||
detailsQueryKeyPrefix: 'job',
|
||||
},
|
||||
details: {
|
||||
category: InfraMonitoringEntity.JOBS,
|
||||
eventCategory: InfraMonitoringEvents.Job,
|
||||
queryKeyPrefix: 'job',
|
||||
getSelectedItemExpression: k8sJobGetSelectedItemExpression,
|
||||
fetchEntityData,
|
||||
getEntityName: k8sJobGetEntityName,
|
||||
getInitialLogTracesExpression: k8sJobInitialLogTracesExpression,
|
||||
getInitialEventsExpression: k8sJobInitialEventsExpression,
|
||||
metadataConfig: k8sJobDetailsMetadataConfig,
|
||||
entityWidgetInfo: jobWidgetInfo,
|
||||
getEntityQueryPayload: getJobMetricsQueryPayload,
|
||||
customTabs: [
|
||||
createPodMetricsTab<InframonitoringtypesJobRecordDTO>({
|
||||
getQueryPayload: getJobPodMetricsQueryPayload,
|
||||
category: InfraMonitoringEntity.JOBS,
|
||||
queryKey: 'jobPodMetrics',
|
||||
docBasePath: '/infrastructure-monitoring/kubernetes/jobs/',
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -113,17 +113,12 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ row, rowId }): React.ReactNode => {
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByStatus = row.podCountsByStatus;
|
||||
if (!podCountsByStatus) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
items={getPodStatusItems(podCountsByStatus)}
|
||||
rowId={rowId}
|
||||
/>
|
||||
);
|
||||
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -137,7 +132,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
width: { min: 210 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ row, rowId }): React.ReactNode => (
|
||||
cell: ({ row }): React.ReactNode => (
|
||||
<GroupedStatusCounts
|
||||
items={[
|
||||
{ value: row.activePods, label: 'Active', color: Color.BG_ROBIN_500 },
|
||||
@@ -153,7 +148,6 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
color: Color.BG_AMBER_500,
|
||||
},
|
||||
]}
|
||||
rowId={rowId}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listNamespaces } from 'api/generated/services/inframonitoring';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
InframonitoringtypesNamespaceRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
getNamespaceMetricsQueryPayload,
|
||||
getNamespacePodMetricsQueryPayload,
|
||||
k8sNamespaceDetailsCountsConfig,
|
||||
k8sNamespaceDetailsMetadataConfig,
|
||||
k8sNamespaceGetCountsFilterExpression,
|
||||
k8sNamespaceGetEntityName,
|
||||
k8sNamespaceGetSelectedItemExpression,
|
||||
k8sNamespaceInitialEventsExpression,
|
||||
k8sNamespaceInitialLogTracesExpression,
|
||||
namespaceWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sNamespaceItemKey,
|
||||
getK8sNamespaceRowKey,
|
||||
k8sNamespacesColumnsConfig,
|
||||
} from './table.config';
|
||||
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
|
||||
|
||||
function K8sNamespacesList({
|
||||
controlListPrefix,
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
try {
|
||||
const response = await listNamespaces(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesNamespaceRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{
|
||||
data: InframonitoringtypesNamespaceRecordDTO | null;
|
||||
error?: APIError | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listNamespaces(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const customTabs = useMemo(
|
||||
() => [
|
||||
createPodMetricsTab<InframonitoringtypesNamespaceRecordDTO>({
|
||||
getQueryPayload: getNamespacePodMetricsQueryPayload,
|
||||
category: InfraMonitoringEntity.NAMESPACES,
|
||||
queryKey: 'namespacePodMetrics',
|
||||
}),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO, SelectedItemParams>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.NAMESPACES}
|
||||
tableColumns={k8sNamespacesColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
getRowKey={getK8sNamespaceRowKey}
|
||||
getItemKey={getK8sNamespaceItemKey}
|
||||
eventCategory={InfraMonitoringEvents.Namespace}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<InframonitoringtypesNamespaceRecordDTO>
|
||||
category={InfraMonitoringEntity.NAMESPACES}
|
||||
eventCategory={InfraMonitoringEvents.Namespace}
|
||||
getSelectedItemExpression={k8sNamespaceGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sNamespaceGetEntityName}
|
||||
getInitialLogTracesExpression={k8sNamespaceInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sNamespaceInitialEventsExpression}
|
||||
metadataConfig={k8sNamespaceDetailsMetadataConfig}
|
||||
countsConfig={k8sNamespaceDetailsCountsConfig}
|
||||
getCountsFilterExpression={k8sNamespaceGetCountsFilterExpression}
|
||||
entityWidgetInfo={namespaceWidgetInfo}
|
||||
getEntityQueryPayload={getNamespaceMetricsQueryPayload}
|
||||
queryKeyPrefix="namespace"
|
||||
customTabs={customTabs}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sNamespacesList;
|
||||
@@ -1,159 +0,0 @@
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listNamespaces } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesNamespaceRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
|
||||
|
||||
import { K8sEntityConfig } from '../Base/entity.config.types';
|
||||
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
getNamespaceMetricsQueryPayload,
|
||||
getNamespacePodMetricsQueryPayload,
|
||||
k8sNamespaceDetailsCountsConfig,
|
||||
k8sNamespaceDetailsMetadataConfig,
|
||||
k8sNamespaceGetCountsFilterExpression,
|
||||
k8sNamespaceGetEntityName,
|
||||
k8sNamespaceGetSelectedItemExpression,
|
||||
k8sNamespaceInitialEventsExpression,
|
||||
k8sNamespaceInitialLogTracesExpression,
|
||||
namespaceWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sNamespaceItemKey,
|
||||
getK8sNamespaceRowKey,
|
||||
k8sNamespacesColumnsConfig,
|
||||
} from './table.config';
|
||||
|
||||
async function fetchListData(
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<
|
||||
InframonitoringtypesNamespaceRecordDTO,
|
||||
SelectedItemParams
|
||||
>['list']['fetchListData']
|
||||
> {
|
||||
try {
|
||||
const response = await listNamespaces(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesNamespaceRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEntityData(
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<
|
||||
InframonitoringtypesNamespaceRecordDTO,
|
||||
SelectedItemParams
|
||||
>['details']['fetchEntityData']
|
||||
> {
|
||||
try {
|
||||
const response = await listNamespaces(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const namespaceCustomTabs = [
|
||||
createPodMetricsTab<InframonitoringtypesNamespaceRecordDTO>({
|
||||
getQueryPayload: getNamespacePodMetricsQueryPayload,
|
||||
category: InfraMonitoringEntity.NAMESPACES,
|
||||
queryKey: 'namespacePodMetrics',
|
||||
docBasePath: '/infrastructure-monitoring/kubernetes/namespaces/',
|
||||
}),
|
||||
];
|
||||
|
||||
export const namespaceEntityConfig: K8sEntityConfig<
|
||||
InframonitoringtypesNamespaceRecordDTO,
|
||||
SelectedItemParams
|
||||
> = {
|
||||
list: {
|
||||
entity: InfraMonitoringEntity.NAMESPACES,
|
||||
eventCategory: InfraMonitoringEvents.Namespace,
|
||||
tableColumns: k8sNamespacesColumnsConfig,
|
||||
fetchListData,
|
||||
getRowKey: getK8sNamespaceRowKey,
|
||||
getItemKey: getK8sNamespaceItemKey,
|
||||
detailsQueryKeyPrefix: 'namespace',
|
||||
},
|
||||
details: {
|
||||
category: InfraMonitoringEntity.NAMESPACES,
|
||||
eventCategory: InfraMonitoringEvents.Namespace,
|
||||
queryKeyPrefix: 'namespace',
|
||||
getSelectedItemExpression: k8sNamespaceGetSelectedItemExpression,
|
||||
fetchEntityData,
|
||||
getEntityName: k8sNamespaceGetEntityName,
|
||||
getInitialLogTracesExpression: k8sNamespaceInitialLogTracesExpression,
|
||||
getInitialEventsExpression: k8sNamespaceInitialEventsExpression,
|
||||
metadataConfig: k8sNamespaceDetailsMetadataConfig,
|
||||
countsConfig: k8sNamespaceDetailsCountsConfig,
|
||||
getCountsFilterExpression: k8sNamespaceGetCountsFilterExpression,
|
||||
entityWidgetInfo: namespaceWidgetInfo,
|
||||
getEntityQueryPayload: getNamespaceMetricsQueryPayload,
|
||||
customTabs: namespaceCustomTabs,
|
||||
},
|
||||
};
|
||||
@@ -114,16 +114,13 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
|
||||
row.podCountsByStatus,
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
cell: ({ row, rowId }): React.ReactNode => {
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByStatus = row.podCountsByStatus;
|
||||
if (!podCountsByStatus) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
items={getPodStatusItems(row.podCountsByStatus)}
|
||||
rowId={rowId}
|
||||
/>
|
||||
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { useCallback } from 'react';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listNodes } from 'api/generated/services/inframonitoring';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
InframonitoringtypesNodeRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
getNodeMetricsQueryPayload,
|
||||
k8sNodeDetailsMetadataConfig,
|
||||
k8sNodeGetEntityName,
|
||||
k8sNodeGetSelectedItemExpression,
|
||||
k8sNodeInitialEventsExpression,
|
||||
k8sNodeInitialLogTracesExpression,
|
||||
nodeWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sNodeItemKey,
|
||||
getK8sNodeRowKey,
|
||||
k8sNodesColumnsConfig,
|
||||
} from './table.config';
|
||||
|
||||
function K8sNodesList({
|
||||
controlListPrefix,
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
try {
|
||||
const response = await listNodes(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesNodeRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{
|
||||
data: InframonitoringtypesNodeRecordDTO | null;
|
||||
error?: APIError | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listNodes(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesNodeRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.NODES}
|
||||
tableColumns={k8sNodesColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
getRowKey={getK8sNodeRowKey}
|
||||
getItemKey={getK8sNodeItemKey}
|
||||
eventCategory={InfraMonitoringEvents.Node}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<InframonitoringtypesNodeRecordDTO>
|
||||
category={InfraMonitoringEntity.NODES}
|
||||
eventCategory={InfraMonitoringEvents.Node}
|
||||
getSelectedItemExpression={k8sNodeGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sNodeGetEntityName}
|
||||
getInitialLogTracesExpression={k8sNodeInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sNodeInitialEventsExpression}
|
||||
metadataConfig={k8sNodeDetailsMetadataConfig}
|
||||
entityWidgetInfo={nodeWidgetInfo}
|
||||
getEntityQueryPayload={getNodeMetricsQueryPayload}
|
||||
queryKeyPrefix="node"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sNodesList;
|
||||
@@ -1,142 +0,0 @@
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listNodes } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesNodeRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
|
||||
import { K8sEntityConfig } from '../Base/entity.config.types';
|
||||
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
getNodeMetricsQueryPayload,
|
||||
k8sNodeDetailsMetadataConfig,
|
||||
k8sNodeGetEntityName,
|
||||
k8sNodeGetSelectedItemExpression,
|
||||
k8sNodeInitialEventsExpression,
|
||||
k8sNodeInitialLogTracesExpression,
|
||||
nodeWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sNodeItemKey,
|
||||
getK8sNodeRowKey,
|
||||
k8sNodesColumnsConfig,
|
||||
} from './table.config';
|
||||
|
||||
async function fetchListData(
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<
|
||||
InframonitoringtypesNodeRecordDTO,
|
||||
string
|
||||
>['list']['fetchListData']
|
||||
> {
|
||||
try {
|
||||
const response = await listNodes(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesNodeRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEntityData(
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<
|
||||
InframonitoringtypesNodeRecordDTO,
|
||||
string
|
||||
>['details']['fetchEntityData']
|
||||
> {
|
||||
try {
|
||||
const response = await listNodes(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const nodeEntityConfig: K8sEntityConfig<
|
||||
InframonitoringtypesNodeRecordDTO,
|
||||
string
|
||||
> = {
|
||||
list: {
|
||||
entity: InfraMonitoringEntity.NODES,
|
||||
eventCategory: InfraMonitoringEvents.Node,
|
||||
tableColumns: k8sNodesColumnsConfig,
|
||||
fetchListData,
|
||||
getRowKey: getK8sNodeRowKey,
|
||||
getItemKey: getK8sNodeItemKey,
|
||||
detailsQueryKeyPrefix: 'node',
|
||||
},
|
||||
details: {
|
||||
category: InfraMonitoringEntity.NODES,
|
||||
eventCategory: InfraMonitoringEvents.Node,
|
||||
queryKeyPrefix: 'node',
|
||||
getSelectedItemExpression: k8sNodeGetSelectedItemExpression,
|
||||
fetchEntityData,
|
||||
getEntityName: k8sNodeGetEntityName,
|
||||
getInitialLogTracesExpression: k8sNodeInitialLogTracesExpression,
|
||||
getInitialEventsExpression: k8sNodeInitialEventsExpression,
|
||||
metadataConfig: k8sNodeDetailsMetadataConfig,
|
||||
entityWidgetInfo: nodeWidgetInfo,
|
||||
getEntityQueryPayload: getNodeMetricsQueryPayload,
|
||||
},
|
||||
};
|
||||
@@ -98,7 +98,7 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
|
||||
accessorFn: (row): string => row.condition,
|
||||
width: { min: 120 },
|
||||
enableSort: false,
|
||||
cell: ({ row, groupMeta, rowId }): React.ReactNode => {
|
||||
cell: ({ row, groupMeta }): React.ReactNode => {
|
||||
if (!groupMeta) {
|
||||
const color =
|
||||
NODE_CONDITION_COLORS[row.condition] || NODE_CONDITION_COLORS.no_data;
|
||||
@@ -123,7 +123,6 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
|
||||
color: Color.BG_AMBER_500,
|
||||
},
|
||||
]}
|
||||
rowId={rowId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -139,16 +138,13 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
|
||||
row.podCountsByStatus,
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
cell: ({ row, rowId }): React.ReactNode => {
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByStatus = row.podCountsByStatus;
|
||||
if (!podCountsByStatus) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
items={getPodStatusItems(row.podCountsByStatus)}
|
||||
rowId={rowId}
|
||||
/>
|
||||
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
149
frontend/src/container/InfraMonitoringK8sV2/Pods/K8sPodLists.tsx
generated
Normal file
149
frontend/src/container/InfraMonitoringK8sV2/Pods/K8sPodLists.tsx
generated
Normal file
@@ -0,0 +1,149 @@
|
||||
import { useCallback } from 'react';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listPods } from 'api/generated/services/inframonitoring';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
InframonitoringtypesPodRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
getPodMetricsQueryPayload,
|
||||
k8sPodDetailsMetadataConfig,
|
||||
k8sPodGetEntityName,
|
||||
k8sPodGetSelectedItemExpression,
|
||||
k8sPodInitialEventsExpression,
|
||||
k8sPodInitialLogTracesExpression,
|
||||
podWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sPodItemKey,
|
||||
getK8sPodRowKey,
|
||||
k8sPodColumnsConfig,
|
||||
} from './table.config';
|
||||
|
||||
function K8sPodsList({
|
||||
controlListPrefix,
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
try {
|
||||
const response = await listPods(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesPodRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{
|
||||
data: InframonitoringtypesPodRecordDTO | null;
|
||||
error?: APIError | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listPods(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesPodRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
tableColumns={k8sPodColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
getRowKey={getK8sPodRowKey}
|
||||
getItemKey={getK8sPodItemKey}
|
||||
eventCategory={InfraMonitoringEvents.Pod}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<InframonitoringtypesPodRecordDTO>
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
eventCategory={InfraMonitoringEvents.Pod}
|
||||
getSelectedItemExpression={k8sPodGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sPodGetEntityName}
|
||||
getInitialLogTracesExpression={k8sPodInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sPodInitialEventsExpression}
|
||||
metadataConfig={k8sPodDetailsMetadataConfig}
|
||||
entityWidgetInfo={podWidgetInfo}
|
||||
getEntityQueryPayload={getPodMetricsQueryPayload}
|
||||
queryKeyPrefix="pod"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sPodsList;
|
||||
@@ -1,134 +0,0 @@
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listPods } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesPodRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
|
||||
import { K8sEntityConfig } from '../Base/entity.config.types';
|
||||
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
getPodMetricsQueryPayload,
|
||||
k8sPodDetailsMetadataConfig,
|
||||
k8sPodGetEntityName,
|
||||
k8sPodGetSelectedItemExpression,
|
||||
k8sPodInitialEventsExpression,
|
||||
k8sPodInitialLogTracesExpression,
|
||||
podWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sPodItemKey,
|
||||
getK8sPodRowKey,
|
||||
k8sPodColumnsConfig,
|
||||
} from './table.config';
|
||||
|
||||
async function fetchListData(
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<InframonitoringtypesPodRecordDTO>['list']['fetchListData']
|
||||
> {
|
||||
try {
|
||||
const response = await listPods(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesPodRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEntityData(
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<InframonitoringtypesPodRecordDTO>['details']['fetchEntityData']
|
||||
> {
|
||||
try {
|
||||
const response = await listPods(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const podEntityConfig: K8sEntityConfig<InframonitoringtypesPodRecordDTO> =
|
||||
{
|
||||
list: {
|
||||
entity: InfraMonitoringEntity.PODS,
|
||||
eventCategory: InfraMonitoringEvents.Pod,
|
||||
tableColumns: k8sPodColumnsConfig,
|
||||
fetchListData,
|
||||
getRowKey: getK8sPodRowKey,
|
||||
getItemKey: getK8sPodItemKey,
|
||||
detailsQueryKeyPrefix: 'pod',
|
||||
},
|
||||
details: {
|
||||
category: InfraMonitoringEntity.PODS,
|
||||
eventCategory: InfraMonitoringEvents.Pod,
|
||||
queryKeyPrefix: 'pod',
|
||||
getSelectedItemExpression: k8sPodGetSelectedItemExpression,
|
||||
fetchEntityData,
|
||||
getEntityName: k8sPodGetEntityName,
|
||||
getInitialLogTracesExpression: k8sPodInitialLogTracesExpression,
|
||||
getInitialEventsExpression: k8sPodInitialEventsExpression,
|
||||
metadataConfig: k8sPodDetailsMetadataConfig,
|
||||
entityWidgetInfo: podWidgetInfo,
|
||||
getEntityQueryPayload: getPodMetricsQueryPayload,
|
||||
},
|
||||
};
|
||||
@@ -131,16 +131,13 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
visibilityBehavior: 'hidden-on-collapse',
|
||||
cell: ({ row, rowId }): React.ReactNode => {
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByStatus = row.podCountsByStatus;
|
||||
if (!podCountsByStatus) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
items={getPodStatusItems(row.podCountsByStatus)}
|
||||
rowId={rowId}
|
||||
/>
|
||||
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -174,7 +171,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.podRestarts,
|
||||
width: { min: 140 },
|
||||
width: { min: 100 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const restarts = value as number;
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listStatefulSets } from 'api/generated/services/inframonitoring';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
InframonitoringtypesStatefulSetRecordDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
getStatefulSetMetricsQueryPayload,
|
||||
getStatefulSetPodMetricsQueryPayload,
|
||||
k8sStatefulSetDetailsMetadataConfig,
|
||||
k8sStatefulSetGetEntityName,
|
||||
k8sStatefulSetGetSelectedItemExpression,
|
||||
k8sStatefulSetInitialEventsExpression,
|
||||
k8sStatefulSetInitialLogTracesExpression,
|
||||
statefulSetWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sStatefulSetItemKey,
|
||||
getK8sStatefulSetRowKey,
|
||||
k8sStatefulSetsColumnsConfig,
|
||||
} from './table.config';
|
||||
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
|
||||
|
||||
function K8sStatefulSetsList({
|
||||
controlListPrefix,
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
try {
|
||||
const response = await listStatefulSets(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesStatefulSetRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{
|
||||
data: InframonitoringtypesStatefulSetRecordDTO | null;
|
||||
error?: APIError | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listStatefulSets(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const customTabs = useMemo(
|
||||
() => [
|
||||
createPodMetricsTab<InframonitoringtypesStatefulSetRecordDTO>({
|
||||
getQueryPayload: getStatefulSetPodMetricsQueryPayload,
|
||||
category: InfraMonitoringEntity.STATEFULSETS,
|
||||
queryKey: 'statefulSetPodMetrics',
|
||||
}),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesStatefulSetRecordDTO, SelectedItemParams>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
tableColumns={k8sStatefulSetsColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
getRowKey={getK8sStatefulSetRowKey}
|
||||
getItemKey={getK8sStatefulSetItemKey}
|
||||
eventCategory={InfraMonitoringEvents.StatefulSet}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<InframonitoringtypesStatefulSetRecordDTO>
|
||||
category={InfraMonitoringEntity.STATEFULSETS}
|
||||
eventCategory={InfraMonitoringEvents.StatefulSet}
|
||||
getSelectedItemExpression={k8sStatefulSetGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sStatefulSetGetEntityName}
|
||||
getInitialLogTracesExpression={k8sStatefulSetInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sStatefulSetInitialEventsExpression}
|
||||
metadataConfig={k8sStatefulSetDetailsMetadataConfig}
|
||||
entityWidgetInfo={statefulSetWidgetInfo}
|
||||
getEntityQueryPayload={getStatefulSetMetricsQueryPayload}
|
||||
queryKeyPrefix="statefulSet"
|
||||
customTabs={customTabs}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sStatefulSetsList;
|
||||
@@ -1,153 +0,0 @@
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listStatefulSets } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
InframonitoringtypesStatefulSetRecordDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
|
||||
|
||||
import { K8sEntityConfig } from '../Base/entity.config.types';
|
||||
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
getStatefulSetMetricsQueryPayload,
|
||||
getStatefulSetPodMetricsQueryPayload,
|
||||
k8sStatefulSetDetailsMetadataConfig,
|
||||
k8sStatefulSetGetEntityName,
|
||||
k8sStatefulSetGetSelectedItemExpression,
|
||||
k8sStatefulSetInitialEventsExpression,
|
||||
k8sStatefulSetInitialLogTracesExpression,
|
||||
statefulSetWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sStatefulSetItemKey,
|
||||
getK8sStatefulSetRowKey,
|
||||
k8sStatefulSetsColumnsConfig,
|
||||
} from './table.config';
|
||||
|
||||
async function fetchListData(
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<
|
||||
InframonitoringtypesStatefulSetRecordDTO,
|
||||
SelectedItemParams
|
||||
>['list']['fetchListData']
|
||||
> {
|
||||
try {
|
||||
const response = await listStatefulSets(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesStatefulSetRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEntityData(
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<
|
||||
InframonitoringtypesStatefulSetRecordDTO,
|
||||
SelectedItemParams
|
||||
>['details']['fetchEntityData']
|
||||
> {
|
||||
try {
|
||||
const response = await listStatefulSets(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const statefulSetEntityConfig: K8sEntityConfig<
|
||||
InframonitoringtypesStatefulSetRecordDTO,
|
||||
SelectedItemParams
|
||||
> = {
|
||||
list: {
|
||||
entity: InfraMonitoringEntity.STATEFULSETS,
|
||||
eventCategory: InfraMonitoringEvents.StatefulSet,
|
||||
tableColumns: k8sStatefulSetsColumnsConfig,
|
||||
fetchListData,
|
||||
getRowKey: getK8sStatefulSetRowKey,
|
||||
getItemKey: getK8sStatefulSetItemKey,
|
||||
detailsQueryKeyPrefix: 'statefulSet',
|
||||
},
|
||||
details: {
|
||||
category: InfraMonitoringEntity.STATEFULSETS,
|
||||
eventCategory: InfraMonitoringEvents.StatefulSet,
|
||||
queryKeyPrefix: 'statefulSet',
|
||||
getSelectedItemExpression: k8sStatefulSetGetSelectedItemExpression,
|
||||
fetchEntityData,
|
||||
getEntityName: k8sStatefulSetGetEntityName,
|
||||
getInitialLogTracesExpression: k8sStatefulSetInitialLogTracesExpression,
|
||||
getInitialEventsExpression: k8sStatefulSetInitialEventsExpression,
|
||||
metadataConfig: k8sStatefulSetDetailsMetadataConfig,
|
||||
entityWidgetInfo: statefulSetWidgetInfo,
|
||||
getEntityQueryPayload: getStatefulSetMetricsQueryPayload,
|
||||
customTabs: [
|
||||
createPodMetricsTab<InframonitoringtypesStatefulSetRecordDTO>({
|
||||
getQueryPayload: getStatefulSetPodMetricsQueryPayload,
|
||||
category: InfraMonitoringEntity.STATEFULSETS,
|
||||
queryKey: 'statefulSetPodMetrics',
|
||||
docBasePath: '/infrastructure-monitoring/kubernetes/statefulsets/',
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -122,17 +122,12 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ row, rowId }): React.ReactNode => {
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByStatus = row.podCountsByStatus;
|
||||
if (!podCountsByStatus) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
items={getPodStatusItems(podCountsByStatus)}
|
||||
rowId={rowId}
|
||||
/>
|
||||
);
|
||||
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -146,7 +141,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
width: { min: 140 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ row, rowId }): React.ReactNode => (
|
||||
cell: ({ row }): React.ReactNode => (
|
||||
<GroupedStatusCounts
|
||||
items={[
|
||||
{
|
||||
@@ -160,7 +155,6 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
color: Color.BG_ROBIN_500,
|
||||
},
|
||||
]}
|
||||
rowId={rowId}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { useCallback } from 'react';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listVolumes } from 'api/generated/services/inframonitoring';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
InframonitoringtypesVolumeRecordDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
getVolumeMetricsQueryPayload,
|
||||
k8sVolumeDetailsMetadataConfig,
|
||||
k8sVolumeGetEntityName,
|
||||
k8sVolumeGetSelectedItemExpression,
|
||||
k8sVolumeInitialEventsExpression,
|
||||
k8sVolumeInitialLogTracesExpression,
|
||||
volumeWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sVolumeItemKey,
|
||||
getK8sVolumeRowKey,
|
||||
k8sVolumesColumnsConfig,
|
||||
} from './table.config';
|
||||
|
||||
function K8sVolumesList({
|
||||
controlListPrefix,
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
try {
|
||||
const response = await listVolumes(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesVolumeRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{
|
||||
data: InframonitoringtypesVolumeRecordDTO | null;
|
||||
error?: APIError | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listVolumes(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesVolumeRecordDTO, SelectedItemParams>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.VOLUMES}
|
||||
tableColumns={k8sVolumesColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
getRowKey={getK8sVolumeRowKey}
|
||||
getItemKey={getK8sVolumeItemKey}
|
||||
eventCategory={InfraMonitoringEvents.Volumes}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<InframonitoringtypesVolumeRecordDTO>
|
||||
category={InfraMonitoringEntity.VOLUMES}
|
||||
eventCategory={InfraMonitoringEvents.Volume}
|
||||
getSelectedItemExpression={k8sVolumeGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sVolumeGetEntityName}
|
||||
getInitialLogTracesExpression={k8sVolumeInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sVolumeInitialEventsExpression}
|
||||
metadataConfig={k8sVolumeDetailsMetadataConfig}
|
||||
entityWidgetInfo={volumeWidgetInfo}
|
||||
getEntityQueryPayload={getVolumeMetricsQueryPayload}
|
||||
queryKeyPrefix="volume"
|
||||
hideDetailViewTabs
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sVolumesList;
|
||||
@@ -1,141 +0,0 @@
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { listVolumes } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
InframonitoringtypesVolumeRecordDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
|
||||
import { K8sEntityConfig } from '../Base/entity.config.types';
|
||||
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
getVolumeMetricsQueryPayload,
|
||||
k8sVolumeDetailsMetadataConfig,
|
||||
k8sVolumeGetEntityName,
|
||||
k8sVolumeGetSelectedItemExpression,
|
||||
k8sVolumeInitialEventsExpression,
|
||||
k8sVolumeInitialLogTracesExpression,
|
||||
volumeWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sVolumeItemKey,
|
||||
getK8sVolumeRowKey,
|
||||
k8sVolumesColumnsConfig,
|
||||
} from './table.config';
|
||||
|
||||
async function fetchListData(
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<
|
||||
InframonitoringtypesVolumeRecordDTO,
|
||||
SelectedItemParams
|
||||
>['list']['fetchListData']
|
||||
> {
|
||||
try {
|
||||
const response = await listVolumes(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesVolumeRecordDTO[],
|
||||
total: 0,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEntityData(
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): ReturnType<
|
||||
K8sEntityConfig<InframonitoringtypesVolumeRecordDTO>['details']['fetchEntityData']
|
||||
> {
|
||||
try {
|
||||
const response = await listVolumes(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error:
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const volumeEntityConfig: K8sEntityConfig<
|
||||
InframonitoringtypesVolumeRecordDTO,
|
||||
SelectedItemParams
|
||||
> = {
|
||||
list: {
|
||||
entity: InfraMonitoringEntity.VOLUMES,
|
||||
eventCategory: InfraMonitoringEvents.Volumes,
|
||||
tableColumns: k8sVolumesColumnsConfig,
|
||||
fetchListData,
|
||||
getRowKey: getK8sVolumeRowKey,
|
||||
getItemKey: getK8sVolumeItemKey,
|
||||
detailsQueryKeyPrefix: 'volume',
|
||||
},
|
||||
details: {
|
||||
category: InfraMonitoringEntity.VOLUMES,
|
||||
eventCategory: InfraMonitoringEvents.Volume,
|
||||
queryKeyPrefix: 'volume',
|
||||
getSelectedItemExpression: k8sVolumeGetSelectedItemExpression,
|
||||
fetchEntityData,
|
||||
getEntityName: k8sVolumeGetEntityName,
|
||||
getInitialLogTracesExpression: k8sVolumeInitialLogTracesExpression,
|
||||
getInitialEventsExpression: k8sVolumeInitialEventsExpression,
|
||||
metadataConfig: k8sVolumeDetailsMetadataConfig,
|
||||
entityWidgetInfo: volumeWidgetInfo,
|
||||
getEntityQueryPayload: getVolumeMetricsQueryPayload,
|
||||
hideDetailViewTabs: true,
|
||||
},
|
||||
};
|
||||
@@ -4,24 +4,33 @@
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: inline-flex;
|
||||
.itemWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
// 4ch value slot (old .valueWrapper) + 3px color bar + 4px gap
|
||||
min-width: calc(4ch + 7px);
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: left;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.item::before {
|
||||
content: '';
|
||||
.separator {
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
border-radius: 1px;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--gsc-color);
|
||||
}
|
||||
|
||||
.valueWrapper {
|
||||
min-width: 4ch;
|
||||
}
|
||||
|
||||
.valueWrapperTooltip {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: left;
|
||||
cursor: default;
|
||||
min-width: min-content;
|
||||
}
|
||||
|
||||
.tooltipContent {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
|
||||
import styles from './GroupedStatusCounts.module.scss';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
@@ -16,7 +18,6 @@ export interface StatusCountItem {
|
||||
|
||||
interface GroupedStatusCountsProps {
|
||||
items: StatusCountItem[];
|
||||
rowId: string;
|
||||
showZeroValues?: boolean;
|
||||
}
|
||||
|
||||
@@ -61,7 +62,6 @@ function buildTooltipContent(item: StatusCountItem): React.ReactNode {
|
||||
|
||||
export function GroupedStatusCounts({
|
||||
items,
|
||||
rowId,
|
||||
showZeroValues = true,
|
||||
}: GroupedStatusCountsProps): JSX.Element {
|
||||
const visibleItems =
|
||||
@@ -74,20 +74,21 @@ export function GroupedStatusCounts({
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{visibleItems.map((item) => (
|
||||
<TanStackTable.HoverTooltip
|
||||
key={item.label}
|
||||
rowId={rowId}
|
||||
title={buildTooltipContent(item)}
|
||||
arrow
|
||||
align="start"
|
||||
>
|
||||
<TanStackTable.Text
|
||||
className={styles.item}
|
||||
style={{ '--gsc-color': item.color } as React.CSSProperties}
|
||||
>
|
||||
{item.value || '-'}
|
||||
</TanStackTable.Text>
|
||||
</TanStackTable.HoverTooltip>
|
||||
<div key={item.label} className={styles.itemWrapper}>
|
||||
<div
|
||||
className={styles.separator}
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
<div className={styles.valueWrapper}>
|
||||
<TooltipSimple title={buildTooltipContent(item)} arrow align="start">
|
||||
<span className={styles.valueWrapperTooltip}>
|
||||
<TanStackTable.Text className={styles.value}>
|
||||
{item.value || '-'}
|
||||
</TanStackTable.Text>
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -905,9 +905,6 @@ export const INFRA_MONITORING_K8S_PARAMS_KEYS = {
|
||||
SELECTED_ITEM: 'selectedItem',
|
||||
SELECTED_ITEM_CLUSTER_NAME: 'selectedItemClusterName',
|
||||
SELECTED_ITEM_NAMESPACE_NAME: 'selectedItemNamespaceName',
|
||||
DETAIL_RELATIVE_TIME: 'detailRelativeTime',
|
||||
DETAIL_START_TIME: 'detailStartTime',
|
||||
DETAIL_END_TIME: 'detailEndTime',
|
||||
};
|
||||
|
||||
/** Metric namespace prefixes for /fields/keys and /fields/values APIs */
|
||||
@@ -937,27 +934,22 @@ export const podUtilizationByPodWidgetInfo = [
|
||||
{
|
||||
title: 'CPU Limit Utilization By Pod Name',
|
||||
yAxisUnit: 'percentunit',
|
||||
docPath: '#cpu-limit-utilization-by-pod-name',
|
||||
},
|
||||
{
|
||||
title: 'CPU Request Utilization By Pod Name',
|
||||
yAxisUnit: 'percentunit',
|
||||
docPath: '#cpu-request-utilization-by-pod-name',
|
||||
},
|
||||
{
|
||||
title: 'Memory Limit Utilization By Pod Name',
|
||||
yAxisUnit: 'percentunit',
|
||||
docPath: '#memory-limit-utilization-by-pod-name',
|
||||
},
|
||||
{
|
||||
title: 'Memory Request Utilization By Pod Name',
|
||||
yAxisUnit: 'percentunit',
|
||||
docPath: '#memory-request-utilization-by-pod-name',
|
||||
},
|
||||
{
|
||||
title: 'FileSystem Usage Percentage By Pod Name',
|
||||
yAxisUnit: 'percentunit',
|
||||
docPath: '#filesystem-usage-percentage-by-pod-name',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import AppRoutes from 'AppRoutes';
|
||||
import { AxiosError } from 'axios';
|
||||
import { GlobalTimeStoreAdapter } from 'components/GlobalTimeStoreAdapter/GlobalTimeStoreAdapter';
|
||||
import { ThemeProvider } from 'hooks/useDarkMode';
|
||||
import { configureOverlayScrollbars } from 'lib/configureOverlayScrollbars';
|
||||
import { NuqsAdapter } from 'nuqs/adapters/react';
|
||||
import { AppProvider } from 'providers/App/App';
|
||||
import TimezoneProvider from 'providers/Timezone';
|
||||
@@ -18,8 +17,6 @@ import './ReactI18';
|
||||
|
||||
import 'styles.scss';
|
||||
|
||||
configureOverlayScrollbars();
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { IS_DEV } from 'lib/env';
|
||||
|
||||
const CHROME_DEVTOOLS_TRACK_PERFORMANCE_ENABLED =
|
||||
IS_DEV &&
|
||||
typeof performance !== 'undefined' &&
|
||||
typeof performance.measure === 'function' &&
|
||||
typeof performance.now === 'function';
|
||||
|
||||
export function chromePerformanceNow(): number {
|
||||
return CHROME_DEVTOOLS_TRACK_PERFORMANCE_ENABLED ? performance.now() : 0;
|
||||
}
|
||||
|
||||
export interface ChromePerformanceTrackEntryOptions {
|
||||
trackGroup: string;
|
||||
track: string;
|
||||
color?:
|
||||
| 'primary'
|
||||
| 'primary-light'
|
||||
| 'primary-dark'
|
||||
| 'secondary'
|
||||
| 'secondary-light'
|
||||
| 'secondary-dark'
|
||||
| 'tertiary'
|
||||
| 'tertiary-light'
|
||||
| 'tertiary-dark'
|
||||
| 'warning'
|
||||
| 'error';
|
||||
tooltipText?: string;
|
||||
properties?: [string, string][];
|
||||
}
|
||||
|
||||
export function chromePerformanceMeasure(
|
||||
name: string,
|
||||
start: number,
|
||||
{
|
||||
trackGroup,
|
||||
track,
|
||||
color = 'primary',
|
||||
tooltipText,
|
||||
properties,
|
||||
}: ChromePerformanceTrackEntryOptions,
|
||||
): void {
|
||||
if (!CHROME_DEVTOOLS_TRACK_PERFORMANCE_ENABLED) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
performance.measure(name, {
|
||||
start,
|
||||
detail: {
|
||||
devtools: {
|
||||
dataType: 'track-entry',
|
||||
trackGroup,
|
||||
track,
|
||||
color,
|
||||
tooltipText,
|
||||
properties,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// User Timing rejected detail shape — ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function createChromePerformanceInteractionTracker(
|
||||
trackGroup: string,
|
||||
track: string,
|
||||
name: string,
|
||||
): {
|
||||
begin: (id: string) => void;
|
||||
end: (id: string) => void;
|
||||
} {
|
||||
const startById = new Map<string, number>();
|
||||
|
||||
return {
|
||||
begin(id: string): void {
|
||||
if (!CHROME_DEVTOOLS_TRACK_PERFORMANCE_ENABLED) {
|
||||
return;
|
||||
}
|
||||
startById.set(id, performance.now());
|
||||
},
|
||||
end(id: string): void {
|
||||
if (!CHROME_DEVTOOLS_TRACK_PERFORMANCE_ENABLED) {
|
||||
return;
|
||||
}
|
||||
const start = startById.get(id);
|
||||
if (start === undefined) {
|
||||
return;
|
||||
}
|
||||
startById.delete(id);
|
||||
chromePerformanceMeasure(name, start, {
|
||||
trackGroup,
|
||||
track,
|
||||
color: 'tertiary',
|
||||
properties: [['id', id]],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { OverlayScrollbars } from 'overlayscrollbars';
|
||||
|
||||
/**
|
||||
* Disables the content `elementEvents` option (default: `[['img', 'load']]`)
|
||||
* for every OverlayScrollbars instance.
|
||||
*
|
||||
* The per-element event cleanups OverlayScrollbars stores in its internal
|
||||
* WeakMap close over the MutationObserver callback scope, which holds arrays
|
||||
* with every node added/removed in that mutation batch. A single long-lived
|
||||
* reference to one of those elements (e.g. CodeMirror's EditContext or its
|
||||
* module-level scratch Range pinning a detached editor) then retains entire
|
||||
* unmounted subtrees as detached DOM — ~1.3k nodes per InfraMonitoring
|
||||
* category switch.
|
||||
*
|
||||
* Content size changes from loading images are still handled by the
|
||||
* scrollbars' size observer, so scrollbar geometry stays correct.
|
||||
*/
|
||||
export function configureOverlayScrollbars(): void {
|
||||
OverlayScrollbars.env().setDefaultOptions({
|
||||
update: { elementEvents: null },
|
||||
});
|
||||
}
|
||||
@@ -23,14 +23,12 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||
import type { MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { cloneDashboardV2 } from 'api/generated/services/dashboard';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import history from 'lib/history';
|
||||
import { DashboardDetailEvents } from 'pages/DashboardPageV2/constants/events';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
@@ -71,7 +69,6 @@ function DashboardActions({
|
||||
}: DashboardActionsProps): JSX.Element {
|
||||
const canEditDashboard = useDashboardStore((s) => s.canEditDashboard);
|
||||
const isLocked = useDashboardStore((s) => s.isLocked);
|
||||
const isEditable = useDashboardStore((s) => s.isEditable);
|
||||
const settingsRequest = useDashboardStore((s) => s.settingsRequest);
|
||||
const clearSettingsRequest = useDashboardStore((s) => s.clearSettingsRequest);
|
||||
const { user } = useAppContext();
|
||||
@@ -115,11 +112,6 @@ function DashboardActions({
|
||||
setIsCloning(true);
|
||||
const response = await cloneDashboardV2({ id: dashboard.id });
|
||||
toast.success('Dashboard cloned');
|
||||
void logEvent(DashboardDetailEvents.Cloned, {
|
||||
dashboardId: dashboard.id,
|
||||
dashboardName: title,
|
||||
source: 'detail',
|
||||
});
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, { dashboardId: response.data.id }),
|
||||
);
|
||||
@@ -128,43 +120,16 @@ function DashboardActions({
|
||||
} finally {
|
||||
setIsCloning(false);
|
||||
}
|
||||
}, [dashboard.id, title, safeNavigate, showErrorModal]);
|
||||
}, [dashboard.id, safeNavigate, showErrorModal]);
|
||||
|
||||
const handleConfirmDelete = useCallback((): void => {
|
||||
deleteDashboardMutation.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
void logEvent(DashboardDetailEvents.Deleted, {
|
||||
dashboardId: dashboard.id,
|
||||
panelCount: Object.keys(dashboard.spec.panels).length,
|
||||
});
|
||||
setIsDeleteOpen(false);
|
||||
history.replace(ROUTES.ALL_DASHBOARD);
|
||||
},
|
||||
});
|
||||
}, [deleteDashboardMutation, dashboard.id, dashboard.spec.panels]);
|
||||
|
||||
const handleOpenSettings = useCallback((): void => {
|
||||
void logEvent(DashboardDetailEvents.SettingsOpened, {
|
||||
dashboardId: dashboard.id,
|
||||
});
|
||||
setIsSettingsDrawerOpen(true);
|
||||
}, [dashboard.id]);
|
||||
|
||||
const handleOpenJsonEditor = useCallback((): void => {
|
||||
void logEvent(DashboardDetailEvents.JsonEditorOpened, {
|
||||
dashboardId: dashboard.id,
|
||||
readOnly: !isEditable,
|
||||
});
|
||||
setIsJsonEditorOpen(true);
|
||||
}, [dashboard.id, isEditable]);
|
||||
|
||||
const handleEnterFullScreen = useCallback((): void => {
|
||||
void logEvent(DashboardDetailEvents.FullScreenToggled, {
|
||||
dashboardId: dashboard.id,
|
||||
enabled: true,
|
||||
});
|
||||
void handle.enter();
|
||||
}, [dashboard.id, handle]);
|
||||
}, [deleteDashboardMutation]);
|
||||
|
||||
// Shown only to edit-permitted users, so the only disabled reason is the lock.
|
||||
const editLabel = useCallback(
|
||||
@@ -211,7 +176,7 @@ function DashboardActions({
|
||||
key: 'fullscreen',
|
||||
label: 'Full screen',
|
||||
icon: <Fullscreen size={14} />,
|
||||
onClick: handleEnterFullScreen,
|
||||
onClick: handle.enter,
|
||||
});
|
||||
|
||||
const items: MenuItem[] = [
|
||||
@@ -263,7 +228,7 @@ function DashboardActions({
|
||||
onOpenRename,
|
||||
handleClone,
|
||||
onLockToggle,
|
||||
handleEnterFullScreen,
|
||||
handle.enter,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -293,7 +258,7 @@ function DashboardActions({
|
||||
prefix={<Configure size="md" />}
|
||||
testId="show-drawer"
|
||||
disabled={isLocked}
|
||||
onClick={handleOpenSettings}
|
||||
onClick={(): void => setIsSettingsDrawerOpen(true)}
|
||||
size="md"
|
||||
>
|
||||
Configure
|
||||
@@ -318,7 +283,7 @@ function DashboardActions({
|
||||
className={styles.toolbarButton}
|
||||
prefix={<Braces size="md" />}
|
||||
testId="edit-json"
|
||||
onClick={handleOpenJsonEditor}
|
||||
onClick={(): void => setIsJsonEditorOpen(true)}
|
||||
size="md"
|
||||
>
|
||||
JSON
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user