mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-07 20:50:26 +01:00
Compare commits
20 Commits
feat/migra
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79c152bc65 | ||
|
|
7c1fe82043 | ||
|
|
d4dbbceab7 | ||
|
|
918cf4dfe5 | ||
|
|
c729ed2637 | ||
|
|
43f7363e84 | ||
|
|
6fe69b94c9 | ||
|
|
1eb27d0bc4 | ||
|
|
55cce3c708 | ||
|
|
d677973d56 | ||
|
|
b5b89bb678 | ||
|
|
b5eab118dd | ||
|
|
98d0bdfe49 | ||
|
|
a71006b662 | ||
|
|
769e36ec84 | ||
|
|
363734054f | ||
|
|
726948db9e | ||
|
|
13249b5e69 | ||
|
|
fec24c2cc3 | ||
|
|
0d3226f363 |
3
.github/workflows/integrationci.yaml
vendored
3
.github/workflows/integrationci.yaml
vendored
@@ -55,6 +55,8 @@ jobs:
|
||||
sqlstore-provider:
|
||||
- postgres
|
||||
- sqlite
|
||||
sqlite-mode:
|
||||
- wal
|
||||
clickhouse-version:
|
||||
- 25.5.6
|
||||
- 25.12.5
|
||||
@@ -102,6 +104,7 @@ jobs:
|
||||
--basetemp=./tmp/ \
|
||||
src/${{matrix.src}} \
|
||||
--sqlstore-provider ${{matrix.sqlstore-provider}} \
|
||||
--sqlite-mode ${{matrix.sqlite-mode}} \
|
||||
--postgres-version ${{matrix.postgres-version}} \
|
||||
--clickhouse-version ${{matrix.clickhouse-version}} \
|
||||
--schema-migrator-version ${{matrix.schema-migrator-version}}
|
||||
|
||||
@@ -86,7 +86,7 @@ sqlstore:
|
||||
# The path to the SQLite database file.
|
||||
path: /var/lib/signoz/signoz.db
|
||||
# The journal mode for the sqlite database. Supported values: delete, wal.
|
||||
mode: delete
|
||||
mode: wal
|
||||
# The timeout for the sqlite database to wait for a lock.
|
||||
busy_timeout: 10s
|
||||
# The default transaction locking behavior. Supported values: deferred, immediate, exclusive.
|
||||
|
||||
@@ -327,27 +327,6 @@ components:
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
AuthtypesStorableRole:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
orgId:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
AuthtypesTransaction:
|
||||
properties:
|
||||
object:
|
||||
@@ -371,7 +350,7 @@ components:
|
||||
id:
|
||||
type: string
|
||||
role:
|
||||
$ref: '#/components/schemas/AuthtypesStorableRole'
|
||||
$ref: '#/components/schemas/AuthtypesRole'
|
||||
roleId:
|
||||
type: string
|
||||
updatedAt:
|
||||
@@ -381,6 +360,11 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- userId
|
||||
- roleId
|
||||
- createdAt
|
||||
- updatedAt
|
||||
- role
|
||||
type: object
|
||||
AuthtypesUserWithRoles:
|
||||
properties:
|
||||
@@ -421,11 +405,11 @@ components:
|
||||
type: object
|
||||
CloudintegrationtypesAWSCollectionStrategy:
|
||||
properties:
|
||||
aws_logs:
|
||||
logs:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesAWSLogsStrategy'
|
||||
aws_metrics:
|
||||
metrics:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesAWSMetricsStrategy'
|
||||
s3_buckets:
|
||||
s3Buckets:
|
||||
additionalProperties:
|
||||
items:
|
||||
type: string
|
||||
@@ -465,12 +449,12 @@ components:
|
||||
type: object
|
||||
CloudintegrationtypesAWSLogsStrategy:
|
||||
properties:
|
||||
cloudwatch_logs_subscriptions:
|
||||
cloudwatchLogsSubscriptions:
|
||||
items:
|
||||
properties:
|
||||
filter_pattern:
|
||||
filterPattern:
|
||||
type: string
|
||||
log_group_name_prefix:
|
||||
logGroupNamePrefix:
|
||||
type: string
|
||||
type: object
|
||||
nullable: true
|
||||
@@ -478,7 +462,7 @@ components:
|
||||
type: object
|
||||
CloudintegrationtypesAWSMetricsStrategy:
|
||||
properties:
|
||||
cloudwatch_metric_stream_filters:
|
||||
cloudwatchMetricStreamFilters:
|
||||
items:
|
||||
properties:
|
||||
MetricNames:
|
||||
@@ -502,7 +486,7 @@ components:
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
s3_buckets:
|
||||
s3Buckets:
|
||||
additionalProperties:
|
||||
items:
|
||||
type: string
|
||||
@@ -577,6 +561,26 @@ components:
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
CloudintegrationtypesCloudIntegrationService:
|
||||
nullable: true
|
||||
properties:
|
||||
cloudIntegrationId:
|
||||
type: string
|
||||
config:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesServiceConfig'
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
type:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesServiceID'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
CloudintegrationtypesCollectedLogAttribute:
|
||||
properties:
|
||||
name:
|
||||
@@ -612,6 +616,16 @@ components:
|
||||
- aws
|
||||
type: object
|
||||
CloudintegrationtypesConnectionArtifactRequest:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesConnectionArtifactRequestConfig'
|
||||
credentials:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesSignozCredentials'
|
||||
required:
|
||||
- config
|
||||
- credentials
|
||||
type: object
|
||||
CloudintegrationtypesConnectionArtifactRequestConfig:
|
||||
properties:
|
||||
aws:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesAWSConnectionArtifactRequest'
|
||||
@@ -710,11 +724,54 @@ components:
|
||||
type: string
|
||||
type: array
|
||||
telemetry:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesAWSCollectionStrategy'
|
||||
$ref: '#/components/schemas/CloudintegrationtypesOldAWSCollectionStrategy'
|
||||
required:
|
||||
- enabled_regions
|
||||
- telemetry
|
||||
type: object
|
||||
CloudintegrationtypesOldAWSCollectionStrategy:
|
||||
properties:
|
||||
aws_logs:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesOldAWSLogsStrategy'
|
||||
aws_metrics:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesOldAWSMetricsStrategy'
|
||||
provider:
|
||||
type: string
|
||||
s3_buckets:
|
||||
additionalProperties:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
type: object
|
||||
CloudintegrationtypesOldAWSLogsStrategy:
|
||||
properties:
|
||||
cloudwatch_logs_subscriptions:
|
||||
items:
|
||||
properties:
|
||||
filter_pattern:
|
||||
type: string
|
||||
log_group_name_prefix:
|
||||
type: string
|
||||
type: object
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
CloudintegrationtypesOldAWSMetricsStrategy:
|
||||
properties:
|
||||
cloudwatch_metric_stream_filters:
|
||||
items:
|
||||
properties:
|
||||
MetricNames:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
Namespace:
|
||||
type: string
|
||||
type: object
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
CloudintegrationtypesPostableAgentCheckInRequest:
|
||||
properties:
|
||||
account_id:
|
||||
@@ -743,6 +800,8 @@ components:
|
||||
properties:
|
||||
assets:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesAssets'
|
||||
cloudIntegrationService:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesCloudIntegrationService'
|
||||
dataCollected:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesDataCollected'
|
||||
icon:
|
||||
@@ -751,9 +810,7 @@ components:
|
||||
type: string
|
||||
overview:
|
||||
type: string
|
||||
serviceConfig:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesServiceConfig'
|
||||
supported_signals:
|
||||
supportedSignals:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesSupportedSignals'
|
||||
telemetryCollectionStrategy:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesCollectionStrategy'
|
||||
@@ -765,9 +822,10 @@ components:
|
||||
- icon
|
||||
- overview
|
||||
- assets
|
||||
- supported_signals
|
||||
- supportedSignals
|
||||
- dataCollected
|
||||
- telemetryCollectionStrategy
|
||||
- cloudIntegrationService
|
||||
type: object
|
||||
CloudintegrationtypesServiceConfig:
|
||||
properties:
|
||||
@@ -776,6 +834,22 @@ components:
|
||||
required:
|
||||
- aws
|
||||
type: object
|
||||
CloudintegrationtypesServiceID:
|
||||
enum:
|
||||
- alb
|
||||
- api-gateway
|
||||
- dynamodb
|
||||
- ec2
|
||||
- ecs
|
||||
- eks
|
||||
- elasticache
|
||||
- lambda
|
||||
- msk
|
||||
- rds
|
||||
- s3sync
|
||||
- sns
|
||||
- sqs
|
||||
type: string
|
||||
CloudintegrationtypesServiceMetadata:
|
||||
properties:
|
||||
enabled:
|
||||
@@ -792,6 +866,22 @@ components:
|
||||
- icon
|
||||
- enabled
|
||||
type: object
|
||||
CloudintegrationtypesSignozCredentials:
|
||||
properties:
|
||||
ingestionKey:
|
||||
type: string
|
||||
ingestionUrl:
|
||||
type: string
|
||||
sigNozApiKey:
|
||||
type: string
|
||||
sigNozApiURL:
|
||||
type: string
|
||||
required:
|
||||
- sigNozApiURL
|
||||
- sigNozApiKey
|
||||
- ingestionUrl
|
||||
- ingestionKey
|
||||
type: object
|
||||
CloudintegrationtypesSupportedSignals:
|
||||
properties:
|
||||
logs:
|
||||
@@ -3410,6 +3500,61 @@ paths:
|
||||
summary: Update account
|
||||
tags:
|
||||
- cloudintegration
|
||||
/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}/services/{service_id}:
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint updates a service for the specified cloud provider
|
||||
operationId: UpdateService
|
||||
parameters:
|
||||
- in: path
|
||||
name: cloud_provider
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: service_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesUpdatableService'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Update service
|
||||
tags:
|
||||
- cloudintegration
|
||||
/api/v1/cloud_integrations/{cloud_provider}/accounts/check_in:
|
||||
post:
|
||||
deprecated: false
|
||||
@@ -3467,6 +3612,59 @@ paths:
|
||||
summary: Agent check-in
|
||||
tags:
|
||||
- cloudintegration
|
||||
/api/v1/cloud_integrations/{cloud_provider}/credentials:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint retrieves the connection credentials required for
|
||||
integration
|
||||
operationId: GetConnectionCredentials
|
||||
parameters:
|
||||
- in: path
|
||||
name: cloud_provider
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesSignozCredentials'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Get connection credentials
|
||||
tags:
|
||||
- cloudintegration
|
||||
/api/v1/cloud_integrations/{cloud_provider}/services:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -3577,55 +3775,6 @@ paths:
|
||||
summary: Get service
|
||||
tags:
|
||||
- cloudintegration
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint updates a service for the specified cloud provider
|
||||
operationId: UpdateService
|
||||
parameters:
|
||||
- in: path
|
||||
name: cloud_provider
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: service_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesUpdatableService'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Update service
|
||||
tags:
|
||||
- cloudintegration
|
||||
/api/v1/complete/google:
|
||||
get:
|
||||
deprecated: false
|
||||
|
||||
@@ -193,6 +193,7 @@ uv run pytest --basetemp=./tmp/ -vv --reuse src/passwordauthn/01_register.py::te
|
||||
Tests can be configured using pytest options:
|
||||
|
||||
- `--sqlstore-provider` - Choose database provider (default: postgres)
|
||||
- `--sqlite-mode` - SQLite journal mode: `delete` or `wal` (default: delete). Only relevant when `--sqlstore-provider=sqlite`.
|
||||
- `--postgres-version` - PostgreSQL version (default: 15)
|
||||
- `--clickhouse-version` - ClickHouse version (default: 25.5.6)
|
||||
- `--zookeeper-version` - Zookeeper version (default: 3.7.1)
|
||||
@@ -202,7 +203,6 @@ Example:
|
||||
uv run pytest --basetemp=./tmp/ -vv --reuse --sqlstore-provider=postgres --postgres-version=14 src/auth/
|
||||
```
|
||||
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- **Always use the `--reuse` flag** when setting up the environment to keep containers running
|
||||
@@ -213,3 +213,4 @@ uv run pytest --basetemp=./tmp/ -vv --reuse --sqlstore-provider=postgres --postg
|
||||
- **Use descriptive test names** that clearly indicate what is being tested
|
||||
- **Leverage fixtures** for common setup and authentication
|
||||
- **Test both success and failure scenarios** to ensure robust functionality
|
||||
- **`--sqlite-mode=wal` does not work on macOS.** The integration test environment runs SigNoz inside a Linux container with the SQLite database file mounted from the macOS host. WAL mode requires shared memory between connections, and connections crossing the VM boundary (macOS host ↔ Linux container) cannot share the WAL index, resulting in `SQLITE_IOERR_SHORT_READ`. WAL mode is tested in CI on Linux only.
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -64,12 +63,12 @@ func NewAnomalyRule(
|
||||
BaseRule: baseRule,
|
||||
}
|
||||
|
||||
switch strings.ToLower(p.RuleCondition.Seasonality) {
|
||||
case "hourly":
|
||||
switch p.RuleCondition.Seasonality {
|
||||
case ruletypes.SeasonalityHourly:
|
||||
t.seasonality = anomaly.SeasonalityHourly
|
||||
case "daily":
|
||||
case ruletypes.SeasonalityDaily:
|
||||
t.seasonality = anomaly.SeasonalityDaily
|
||||
case "weekly":
|
||||
case ruletypes.SeasonalityWeekly:
|
||||
t.seasonality = anomaly.SeasonalityWeekly
|
||||
default:
|
||||
t.seasonality = anomaly.SeasonalityDaily
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestAnomalyRule_NoData_AlertOnAbsent(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
SelectedQuery: "A",
|
||||
Seasonality: "daily",
|
||||
Seasonality: ruletypes.SeasonalityDaily,
|
||||
Thresholds: &ruletypes.RuleThresholdData{
|
||||
Kind: ruletypes.BasicThresholdKind,
|
||||
Spec: ruletypes.BasicRuleThresholds{{
|
||||
@@ -170,7 +170,7 @@ func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
SelectedQuery: "A",
|
||||
Seasonality: "daily",
|
||||
Seasonality: ruletypes.SeasonalityDaily,
|
||||
Thresholds: &ruletypes.RuleThresholdData{
|
||||
Kind: ruletypes.BasicThresholdKind,
|
||||
Spec: ruletypes.BasicRuleThresholds{{
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"@signozhq/checkbox": "0.0.2",
|
||||
"@signozhq/combobox": "0.0.2",
|
||||
"@signozhq/command": "0.0.0",
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/design-tokens": "2.1.1",
|
||||
"@signozhq/dialog": "^0.0.2",
|
||||
"@signozhq/drawer": "0.0.4",
|
||||
"@signozhq/icons": "0.1.0",
|
||||
@@ -68,8 +68,8 @@
|
||||
"@signozhq/toggle-group": "0.0.1",
|
||||
"@signozhq/tooltip": "0.0.2",
|
||||
"@signozhq/ui": "0.0.5",
|
||||
"@tanstack/react-table": "8.20.6",
|
||||
"@tanstack/react-virtual": "3.11.2",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
"@uiw/codemirror-theme-github": "4.24.1",
|
||||
"@uiw/react-codemirror": "4.23.10",
|
||||
|
||||
1
frontend/public/Logos/hasura.svg
Normal file
1
frontend/public/Logos/hasura.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#1eb4d4" viewBox="0 0 24 24"><title>Hasura</title><path d="M23.558 8.172c.707-2.152.282-6.447-1.09-8.032a.42.42 0 0 0-.664.051l-1.69 2.59a1.32 1.32 0 0 1-1.737.276C16.544 1.885 14.354 1.204 12 1.204s-4.544.68-6.378 1.853a1.326 1.326 0 0 1-1.736-.276L2.196.191A.42.42 0 0 0 1.532.14C.16 1.728-.265 6.023.442 8.172c.236.716.3 1.472.16 2.207-.137.73-.276 1.61-.276 2.223C.326 18.898 5.553 24 11.997 24c6.447 0 11.671-5.105 11.671-11.398 0-.613-.138-1.494-.276-2.223a4.47 4.47 0 0 1 .166-2.207m-11.56 13.284c-4.984 0-9.036-3.96-9.036-8.827q0-.239.014-.473c.18-3.316 2.243-6.15 5.16-7.5 1.17-.546 2.481-.848 3.864-.848s2.69.302 3.864.85c2.917 1.351 4.98 4.187 5.16 7.501q.013.236.014.473c-.003 4.864-4.057 8.824-9.04 8.824m3.915-5.43-2.31-3.91-1.98-3.26a.26.26 0 0 0-.223-.125H9.508a.26.26 0 0 0-.227.13.25.25 0 0 0 .003.254l1.895 3.109-2.542 3.787a.25.25 0 0 0-.011.259.26.26 0 0 0 .23.132h1.905a.26.26 0 0 0 .218-.116l1.375-2.096 1.233 2.088a.26.26 0 0 0 .224.127h1.878c.094 0 .18-.049.224-.127a.24.24 0 0 0 0-.251z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
frontend/public/Logos/n8n.svg
Normal file
1
frontend/public/Logos/n8n.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#ea4b71" viewBox="0 0 24 24"><title>n8n</title><path d="M21.474 5.684a2.53 2.53 0 0 0-2.447 1.895H16.13a2.526 2.526 0 0 0-2.492 2.11l-.103.624a1.26 1.26 0 0 1-1.246 1.055h-1.001a2.527 2.527 0 0 0-4.893 0H4.973a2.527 2.527 0 1 0 0 1.264h1.422a2.527 2.527 0 0 0 4.894 0h1a1.26 1.26 0 0 1 1.247 1.055l.103.623a2.526 2.526 0 0 0 2.492 2.111h.37a2.527 2.527 0 1 0 0-1.263h-.37a1.26 1.26 0 0 1-1.246-1.056l-.103-.623A2.52 2.52 0 0 0 13.96 12a2.52 2.52 0 0 0 .82-1.48l.104-.622a1.26 1.26 0 0 1 1.246-1.056h2.896a2.527 2.527 0 1 0 2.447-3.158m0 1.263a1.263 1.263 0 0 1 1.263 1.263 1.263 1.263 0 0 1-1.263 1.264A1.263 1.263 0 0 1 20.21 8.21a1.263 1.263 0 0 1 1.264-1.263m-18.948 3.79A1.263 1.263 0 0 1 3.79 12a1.263 1.263 0 0 1-1.264 1.263A1.263 1.263 0 0 1 1.263 12a1.263 1.263 0 0 1 1.263-1.263m6.316 0A1.263 1.263 0 0 1 10.105 12a1.263 1.263 0 0 1-1.263 1.263A1.263 1.263 0 0 1 7.58 12a1.263 1.263 0 0 1 1.263-1.263m10.106 3.79a1.263 1.263 0 0 1 1.263 1.263 1.263 1.263 0 0 1-1.263 1.263 1.263 1.263 0 0 1-1.264-1.263 1.263 1.263 0 0 1 1.263-1.264"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
frontend/public/Logos/qwen.svg
Normal file
1
frontend/public/Logos/qwen.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.8 KiB |
@@ -1,23 +1,17 @@
|
||||
import { ReactChild, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { ReactChild, useCallback, useMemo } from 'react';
|
||||
import { matchPath, Redirect, useLocation } from 'react-router-dom';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import getAll from 'api/v1/user/get';
|
||||
import { useListUsers } from 'api/generated/services/users';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { ORG_PREFERENCES } from 'constants/orgPreferences';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import history from 'lib/history';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
|
||||
import { OrgPreference } from 'types/api/preferences/preference';
|
||||
import { Organization } from 'types/api/user/getOrganization';
|
||||
import { UserResponse } from 'types/api/user/getUser';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { routePermission } from 'utils/permission';
|
||||
|
||||
@@ -29,6 +23,7 @@ import routes, {
|
||||
SUPPORT_ROUTE,
|
||||
} from './routes';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
const location = useLocation();
|
||||
const { pathname } = location;
|
||||
@@ -61,20 +56,17 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
const currentRoute = mapRoutes.get('current');
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const [orgData, setOrgData] = useState<Organization | undefined>(undefined);
|
||||
const orgData = useMemo(() => {
|
||||
if (org && org.length > 0 && org[0].id !== undefined) {
|
||||
return org[0];
|
||||
}
|
||||
return undefined;
|
||||
}, [org]);
|
||||
|
||||
const { data: usersData, isFetching: isFetchingUsers } = useQuery<
|
||||
SuccessResponseV2<UserResponse[]> | undefined,
|
||||
APIError
|
||||
>({
|
||||
queryFn: () => {
|
||||
if (orgData && orgData.id !== undefined) {
|
||||
return getAll();
|
||||
}
|
||||
return undefined;
|
||||
const { data: usersData, isFetching: isFetchingUsers } = useListUsers({
|
||||
query: {
|
||||
enabled: !isEmpty(orgData) && user.role === 'ADMIN',
|
||||
},
|
||||
queryKey: ['getOrgUser'],
|
||||
enabled: !isEmpty(orgData) && user.role === 'ADMIN',
|
||||
});
|
||||
|
||||
const checkFirstTimeUser = useCallback((): boolean => {
|
||||
@@ -87,214 +79,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
return remainingUsers.length === 1;
|
||||
}, [usersData?.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isCloudUserVal &&
|
||||
!isFetchingOrgPreferences &&
|
||||
orgPreferences &&
|
||||
!isFetchingUsers &&
|
||||
usersData &&
|
||||
usersData.data
|
||||
) {
|
||||
const isOnboardingComplete = orgPreferences?.find(
|
||||
(preference: OrgPreference) =>
|
||||
preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
|
||||
)?.value;
|
||||
|
||||
// Don't redirect to onboarding if workspace has issues (blocked, suspended, or restricted)
|
||||
// User needs access to settings/billing to fix payment issues
|
||||
const isWorkspaceBlocked = trialInfo?.workSpaceBlock;
|
||||
const isWorkspaceSuspended = activeLicense?.state === LicenseState.DEFAULTED;
|
||||
const isWorkspaceAccessRestricted =
|
||||
activeLicense?.state === LicenseState.TERMINATED ||
|
||||
activeLicense?.state === LicenseState.EXPIRED ||
|
||||
activeLicense?.state === LicenseState.CANCELLED;
|
||||
|
||||
const hasWorkspaceIssue =
|
||||
isWorkspaceBlocked || isWorkspaceSuspended || isWorkspaceAccessRestricted;
|
||||
|
||||
if (hasWorkspaceIssue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFirstUser = checkFirstTimeUser();
|
||||
if (
|
||||
isFirstUser &&
|
||||
!isOnboardingComplete &&
|
||||
// if the current route is allowed to be overriden by org onboarding then only do the same
|
||||
!ROUTES_NOT_TO_BE_OVERRIDEN.includes(pathname)
|
||||
) {
|
||||
history.push(ROUTES.ONBOARDING);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
checkFirstTimeUser,
|
||||
isCloudUserVal,
|
||||
isFetchingOrgPreferences,
|
||||
isFetchingUsers,
|
||||
orgPreferences,
|
||||
usersData,
|
||||
pathname,
|
||||
trialInfo?.workSpaceBlock,
|
||||
activeLicense?.state,
|
||||
]);
|
||||
|
||||
const navigateToWorkSpaceBlocked = useCallback((): void => {
|
||||
const isRouteEnabledForWorkspaceBlockedState =
|
||||
isAdmin &&
|
||||
(pathname === ROUTES.SETTINGS ||
|
||||
pathname === ROUTES.ORG_SETTINGS ||
|
||||
pathname === ROUTES.MEMBERS_SETTINGS ||
|
||||
pathname === ROUTES.BILLING ||
|
||||
pathname === ROUTES.MY_SETTINGS);
|
||||
|
||||
if (
|
||||
pathname &&
|
||||
pathname !== ROUTES.WORKSPACE_LOCKED &&
|
||||
!isRouteEnabledForWorkspaceBlockedState
|
||||
) {
|
||||
history.push(ROUTES.WORKSPACE_LOCKED);
|
||||
}
|
||||
}, [isAdmin, pathname]);
|
||||
|
||||
const navigateToWorkSpaceAccessRestricted = useCallback((): void => {
|
||||
if (pathname && pathname !== ROUTES.WORKSPACE_ACCESS_RESTRICTED) {
|
||||
history.push(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetchingActiveLicense && activeLicense) {
|
||||
const isTerminated = activeLicense.state === LicenseState.TERMINATED;
|
||||
const isExpired = activeLicense.state === LicenseState.EXPIRED;
|
||||
const isCancelled = activeLicense.state === LicenseState.CANCELLED;
|
||||
|
||||
const isWorkspaceAccessRestricted = isTerminated || isExpired || isCancelled;
|
||||
|
||||
const { platform } = activeLicense;
|
||||
|
||||
if (isWorkspaceAccessRestricted && platform === LicensePlatform.CLOUD) {
|
||||
navigateToWorkSpaceAccessRestricted();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isFetchingActiveLicense,
|
||||
activeLicense,
|
||||
navigateToWorkSpaceAccessRestricted,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetchingActiveLicense) {
|
||||
const shouldBlockWorkspace = trialInfo?.workSpaceBlock;
|
||||
|
||||
if (
|
||||
shouldBlockWorkspace &&
|
||||
activeLicense?.platform === LicensePlatform.CLOUD
|
||||
) {
|
||||
navigateToWorkSpaceBlocked();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isFetchingActiveLicense,
|
||||
trialInfo?.workSpaceBlock,
|
||||
activeLicense?.platform,
|
||||
navigateToWorkSpaceBlocked,
|
||||
]);
|
||||
|
||||
const navigateToWorkSpaceSuspended = useCallback((): void => {
|
||||
if (pathname && pathname !== ROUTES.WORKSPACE_SUSPENDED) {
|
||||
history.push(ROUTES.WORKSPACE_SUSPENDED);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetchingActiveLicense && activeLicense) {
|
||||
const shouldSuspendWorkspace =
|
||||
activeLicense.state === LicenseState.DEFAULTED;
|
||||
|
||||
if (
|
||||
shouldSuspendWorkspace &&
|
||||
activeLicense.platform === LicensePlatform.CLOUD
|
||||
) {
|
||||
navigateToWorkSpaceSuspended();
|
||||
}
|
||||
}
|
||||
}, [isFetchingActiveLicense, activeLicense, navigateToWorkSpaceSuspended]);
|
||||
|
||||
useEffect(() => {
|
||||
if (org && org.length > 0 && org[0].id !== undefined) {
|
||||
setOrgData(org[0]);
|
||||
}
|
||||
}, [org]);
|
||||
|
||||
// if the feature flag is enabled and the current route is /get-started then redirect to /get-started-with-signoz-cloud
|
||||
useEffect(() => {
|
||||
if (
|
||||
currentRoute?.path === ROUTES.GET_STARTED &&
|
||||
featureFlags?.find((e) => e.name === FeatureKeys.ONBOARDING_V3)?.active
|
||||
) {
|
||||
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
}
|
||||
}, [currentRoute, featureFlags]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
useEffect(() => {
|
||||
// if it is an old route navigate to the new route
|
||||
if (isOldRoute) {
|
||||
// this will be handled by the redirect component below
|
||||
return;
|
||||
}
|
||||
|
||||
// if the current route is public dashboard then don't redirect to login
|
||||
const isPublicDashboard = currentRoute?.path === ROUTES.PUBLIC_DASHBOARD;
|
||||
|
||||
if (isPublicDashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the current route
|
||||
if (currentRoute) {
|
||||
const { isPrivate, key } = currentRoute;
|
||||
if (isPrivate) {
|
||||
if (isLoggedInState) {
|
||||
const route = routePermission[key];
|
||||
if (route && route.find((e) => e === user.role) === undefined) {
|
||||
history.push(ROUTES.UN_AUTHORIZED);
|
||||
}
|
||||
} else {
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
|
||||
history.push(ROUTES.LOGIN);
|
||||
}
|
||||
} else if (isLoggedInState) {
|
||||
const fromPathname = getLocalStorageApi(
|
||||
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
|
||||
);
|
||||
if (fromPathname) {
|
||||
history.push(fromPathname);
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
|
||||
} else if (pathname !== ROUTES.SOMETHING_WENT_WRONG) {
|
||||
history.push(ROUTES.HOME);
|
||||
}
|
||||
} else {
|
||||
// do nothing as the unauthenticated routes are LOGIN and SIGNUP and the LOGIN container takes care of routing to signup if
|
||||
// setup is not completed
|
||||
}
|
||||
} else if (isLoggedInState) {
|
||||
const fromPathname = getLocalStorageApi(
|
||||
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
|
||||
);
|
||||
if (fromPathname) {
|
||||
history.push(fromPathname);
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
|
||||
} else {
|
||||
history.push(ROUTES.HOME);
|
||||
}
|
||||
} else {
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
|
||||
history.push(ROUTES.LOGIN);
|
||||
}
|
||||
}, [isLoggedInState, pathname, user, isOldRoute, currentRoute, location]);
|
||||
|
||||
// Handle old routes - redirect to new routes
|
||||
if (isOldRoute) {
|
||||
const redirectUrl = oldNewRoutesMapping[pathname];
|
||||
return (
|
||||
@@ -308,7 +93,143 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: disabling this rule as there is no need to have div
|
||||
// Public dashboard - no redirect needed
|
||||
const isPublicDashboard = currentRoute?.path === ROUTES.PUBLIC_DASHBOARD;
|
||||
if (isPublicDashboard) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// Check for workspace access restriction (cloud only)
|
||||
const isCloudPlatform = activeLicense?.platform === LicensePlatform.CLOUD;
|
||||
|
||||
if (!isFetchingActiveLicense && activeLicense && isCloudPlatform) {
|
||||
const isTerminated = activeLicense.state === LicenseState.TERMINATED;
|
||||
const isExpired = activeLicense.state === LicenseState.EXPIRED;
|
||||
const isCancelled = activeLicense.state === LicenseState.CANCELLED;
|
||||
const isWorkspaceAccessRestricted = isTerminated || isExpired || isCancelled;
|
||||
|
||||
if (
|
||||
isWorkspaceAccessRestricted &&
|
||||
pathname !== ROUTES.WORKSPACE_ACCESS_RESTRICTED
|
||||
) {
|
||||
return <Redirect to={ROUTES.WORKSPACE_ACCESS_RESTRICTED} />;
|
||||
}
|
||||
|
||||
// Check for workspace suspended (DEFAULTED)
|
||||
const shouldSuspendWorkspace = activeLicense.state === LicenseState.DEFAULTED;
|
||||
if (shouldSuspendWorkspace && pathname !== ROUTES.WORKSPACE_SUSPENDED) {
|
||||
return <Redirect to={ROUTES.WORKSPACE_SUSPENDED} />;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for workspace blocked (trial expired)
|
||||
if (!isFetchingActiveLicense && isCloudPlatform && trialInfo?.workSpaceBlock) {
|
||||
const isRouteEnabledForWorkspaceBlockedState =
|
||||
isAdmin &&
|
||||
(pathname === ROUTES.SETTINGS ||
|
||||
pathname === ROUTES.ORG_SETTINGS ||
|
||||
pathname === ROUTES.MEMBERS_SETTINGS ||
|
||||
pathname === ROUTES.BILLING ||
|
||||
pathname === ROUTES.MY_SETTINGS);
|
||||
|
||||
if (
|
||||
pathname !== ROUTES.WORKSPACE_LOCKED &&
|
||||
!isRouteEnabledForWorkspaceBlockedState
|
||||
) {
|
||||
return <Redirect to={ROUTES.WORKSPACE_LOCKED} />;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for onboarding redirect (cloud users, first user, onboarding not complete)
|
||||
if (
|
||||
isCloudUserVal &&
|
||||
!isFetchingOrgPreferences &&
|
||||
orgPreferences &&
|
||||
!isFetchingUsers &&
|
||||
usersData &&
|
||||
usersData.data
|
||||
) {
|
||||
const isOnboardingComplete = orgPreferences?.find(
|
||||
(preference: OrgPreference) =>
|
||||
preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
|
||||
)?.value;
|
||||
|
||||
// Don't redirect to onboarding if workspace has issues
|
||||
const isWorkspaceBlocked = trialInfo?.workSpaceBlock;
|
||||
const isWorkspaceSuspended = activeLicense?.state === LicenseState.DEFAULTED;
|
||||
const isWorkspaceAccessRestricted =
|
||||
activeLicense?.state === LicenseState.TERMINATED ||
|
||||
activeLicense?.state === LicenseState.EXPIRED ||
|
||||
activeLicense?.state === LicenseState.CANCELLED;
|
||||
|
||||
const hasWorkspaceIssue =
|
||||
isWorkspaceBlocked || isWorkspaceSuspended || isWorkspaceAccessRestricted;
|
||||
|
||||
if (!hasWorkspaceIssue) {
|
||||
const isFirstUser = checkFirstTimeUser();
|
||||
if (
|
||||
isFirstUser &&
|
||||
!isOnboardingComplete &&
|
||||
!ROUTES_NOT_TO_BE_OVERRIDEN.includes(pathname) &&
|
||||
pathname !== ROUTES.ONBOARDING
|
||||
) {
|
||||
return <Redirect to={ROUTES.ONBOARDING} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for GET_STARTED → GET_STARTED_WITH_CLOUD redirect (feature flag)
|
||||
if (
|
||||
currentRoute?.path === ROUTES.GET_STARTED &&
|
||||
featureFlags?.find((e) => e.name === FeatureKeys.ONBOARDING_V3)?.active
|
||||
) {
|
||||
return <Redirect to={ROUTES.GET_STARTED_WITH_CLOUD} />;
|
||||
}
|
||||
|
||||
// Main routing logic
|
||||
if (currentRoute) {
|
||||
const { isPrivate, key } = currentRoute;
|
||||
if (isPrivate) {
|
||||
if (isLoggedInState) {
|
||||
const route = routePermission[key];
|
||||
if (route && route.find((e) => e === user.role) === undefined) {
|
||||
return <Redirect to={ROUTES.UN_AUTHORIZED} />;
|
||||
}
|
||||
} else {
|
||||
// Save current path and redirect to login
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
|
||||
return <Redirect to={ROUTES.LOGIN} />;
|
||||
}
|
||||
} else if (isLoggedInState) {
|
||||
// Non-private route, but user is logged in
|
||||
const fromPathname = getLocalStorageApi(
|
||||
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
|
||||
);
|
||||
if (fromPathname) {
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
|
||||
return <Redirect to={fromPathname} />;
|
||||
}
|
||||
if (pathname !== ROUTES.SOMETHING_WENT_WRONG) {
|
||||
return <Redirect to={ROUTES.HOME} />;
|
||||
}
|
||||
}
|
||||
// Non-private route, user not logged in - let login/signup pages handle it
|
||||
} else if (isLoggedInState) {
|
||||
// Unknown route, logged in
|
||||
const fromPathname = getLocalStorageApi(
|
||||
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
|
||||
);
|
||||
if (fromPathname) {
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
|
||||
return <Redirect to={fromPathname} />;
|
||||
}
|
||||
return <Redirect to={ROUTES.HOME} />;
|
||||
} else {
|
||||
// Unknown route, not logged in
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
|
||||
return <Redirect to={ROUTES.LOGIN} />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { FeatureKeys } from 'constants/features';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { ORG_PREFERENCES } from 'constants/orgPreferences';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { AppContext } from 'providers/App/App';
|
||||
import { IAppContext, IUser } from 'providers/App/types';
|
||||
import {
|
||||
@@ -22,19 +21,6 @@ import { ROLES, USER_ROLES } from 'types/roles';
|
||||
|
||||
import PrivateRoute from '../Private';
|
||||
|
||||
// Mock history module
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
push: jest.fn(),
|
||||
location: { pathname: '/', search: '', hash: '' },
|
||||
listen: jest.fn(),
|
||||
createHref: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockHistoryPush = history.push as jest.Mock;
|
||||
|
||||
// Mock localStorage APIs
|
||||
const mockLocalStorage: Record<string, string> = {};
|
||||
jest.mock('api/browser/localstorage/get', () => ({
|
||||
@@ -67,9 +53,12 @@ jest.mock('hooks/useGetTenantLicense', () => ({
|
||||
|
||||
// Mock react-query for users fetch
|
||||
let mockUsersData: { email: string }[] = [];
|
||||
jest.mock('api/v1/user/get', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => Promise.resolve({ data: mockUsersData })),
|
||||
jest.mock('api/generated/services/users', () => ({
|
||||
...jest.requireActual('api/generated/services/users'),
|
||||
useListUsers: jest.fn(() => ({
|
||||
data: { data: mockUsersData },
|
||||
isFetching: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
@@ -236,20 +225,18 @@ function renderPrivateRoute(options: RenderPrivateRouteOptions = {}): void {
|
||||
}
|
||||
|
||||
// Generic assertion helpers for navigation behavior
|
||||
// Using these allows easier refactoring when switching from history.push to Redirect component
|
||||
// Using location-based assertions since Private.tsx now uses Redirect component
|
||||
|
||||
async function assertRedirectsTo(targetRoute: string): Promise<void> {
|
||||
await waitFor(() => {
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith(targetRoute);
|
||||
expect(screen.getByTestId('location-display')).toHaveTextContent(targetRoute);
|
||||
});
|
||||
}
|
||||
|
||||
function assertNoRedirect(): void {
|
||||
expect(mockHistoryPush).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
function assertDoesNotRedirectTo(targetRoute: string): void {
|
||||
expect(mockHistoryPush).not.toHaveBeenCalledWith(targetRoute);
|
||||
function assertStaysOnRoute(expectedRoute: string): void {
|
||||
expect(screen.getByTestId('location-display')).toHaveTextContent(
|
||||
expectedRoute,
|
||||
);
|
||||
}
|
||||
|
||||
function assertRendersChildren(): void {
|
||||
@@ -347,7 +334,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
assertRendersChildren();
|
||||
assertNoRedirect();
|
||||
assertStaysOnRoute('/public/dashboard/abc123');
|
||||
});
|
||||
|
||||
it('should render children for public dashboard route when logged in without redirecting', () => {
|
||||
@@ -359,7 +346,7 @@ describe('PrivateRoute', () => {
|
||||
assertRendersChildren();
|
||||
// Critical: without the isPublicDashboard early return, logged-in users
|
||||
// would be redirected to HOME due to the non-private route handling
|
||||
assertNoRedirect();
|
||||
assertStaysOnRoute('/public/dashboard/abc123');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -417,7 +404,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
assertRendersChildren();
|
||||
assertNoRedirect();
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
|
||||
it('should redirect to unauthorized when VIEWER tries to access admin-only route /alerts/new', async () => {
|
||||
@@ -526,7 +513,7 @@ describe('PrivateRoute', () => {
|
||||
appContext: { isLoggedIn: true },
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.HOME);
|
||||
assertStaysOnRoute(ROUTES.SOMETHING_WENT_WRONG);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -538,7 +525,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
// Should not redirect - login page handles its own routing
|
||||
assertNoRedirect();
|
||||
assertStaysOnRoute(ROUTES.LOGIN);
|
||||
});
|
||||
|
||||
it('should not redirect when not logged in user visits signup page', () => {
|
||||
@@ -547,7 +534,7 @@ describe('PrivateRoute', () => {
|
||||
appContext: { isLoggedIn: false },
|
||||
});
|
||||
|
||||
assertNoRedirect();
|
||||
assertStaysOnRoute(ROUTES.SIGN_UP);
|
||||
});
|
||||
|
||||
it('should not redirect when not logged in user visits password reset page', () => {
|
||||
@@ -556,7 +543,7 @@ describe('PrivateRoute', () => {
|
||||
appContext: { isLoggedIn: false },
|
||||
});
|
||||
|
||||
assertNoRedirect();
|
||||
assertStaysOnRoute(ROUTES.PASSWORD_RESET);
|
||||
});
|
||||
|
||||
it('should not redirect when not logged in user visits forgot password page', () => {
|
||||
@@ -565,7 +552,7 @@ describe('PrivateRoute', () => {
|
||||
appContext: { isLoggedIn: false },
|
||||
});
|
||||
|
||||
assertNoRedirect();
|
||||
assertStaysOnRoute(ROUTES.FORGOT_PASSWORD);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -654,7 +641,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
// Admin should be able to access settings even when workspace is blocked
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.SETTINGS);
|
||||
});
|
||||
|
||||
it('should allow ADMIN to access /settings/billing when workspace is blocked', () => {
|
||||
@@ -670,7 +657,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.BILLING);
|
||||
});
|
||||
|
||||
it('should allow ADMIN to access /settings/org-settings when workspace is blocked', () => {
|
||||
@@ -686,7 +673,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.ORG_SETTINGS);
|
||||
});
|
||||
|
||||
it('should allow ADMIN to access /settings/members when workspace is blocked', () => {
|
||||
@@ -702,7 +689,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.MEMBERS_SETTINGS);
|
||||
});
|
||||
|
||||
it('should allow ADMIN to access /settings/my-settings when workspace is blocked', () => {
|
||||
@@ -718,7 +705,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.MY_SETTINGS);
|
||||
});
|
||||
|
||||
it('should redirect VIEWER to workspace locked even when trying to access settings', async () => {
|
||||
@@ -829,7 +816,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.WORKSPACE_LOCKED);
|
||||
});
|
||||
|
||||
it('should not redirect self-hosted users to workspace locked even when workSpaceBlock is true', () => {
|
||||
@@ -846,7 +833,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: false,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -916,7 +903,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
assertStaysOnRoute(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
});
|
||||
|
||||
it('should not redirect self-hosted users to workspace access restricted when license is terminated', () => {
|
||||
@@ -933,7 +920,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: false,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
|
||||
it('should not redirect when license is ACTIVE', () => {
|
||||
@@ -950,7 +937,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
|
||||
it('should not redirect when license is EVALUATING', () => {
|
||||
@@ -967,7 +954,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1003,7 +990,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_SUSPENDED);
|
||||
assertStaysOnRoute(ROUTES.WORKSPACE_SUSPENDED);
|
||||
});
|
||||
|
||||
it('should not redirect self-hosted users to workspace suspended when license is defaulted', () => {
|
||||
@@ -1020,7 +1007,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: false,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_SUSPENDED);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1040,6 +1027,11 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
// Wait for the users query to complete and trigger re-render
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
await assertRedirectsTo(ROUTES.ONBOARDING);
|
||||
});
|
||||
|
||||
@@ -1055,7 +1047,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when onboarding is already complete', async () => {
|
||||
@@ -1081,7 +1073,7 @@ describe('PrivateRoute', () => {
|
||||
|
||||
// Critical: if isOnboardingComplete check is broken (always false),
|
||||
// this test would fail because all other conditions for redirect ARE met
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding for non-cloud users', () => {
|
||||
@@ -1096,7 +1088,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: false,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when on /workspace-locked route', () => {
|
||||
@@ -1111,7 +1103,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
assertStaysOnRoute(ROUTES.WORKSPACE_LOCKED);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when on /workspace-suspended route', () => {
|
||||
@@ -1126,7 +1118,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
assertStaysOnRoute(ROUTES.WORKSPACE_SUSPENDED);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when workspace is blocked and accessing billing', async () => {
|
||||
@@ -1153,7 +1145,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
// Should NOT redirect to onboarding - user needs to access billing to fix payment
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
assertStaysOnRoute(ROUTES.BILLING);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when workspace is blocked and accessing settings', async () => {
|
||||
@@ -1177,7 +1169,7 @@ describe('PrivateRoute', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
assertStaysOnRoute(ROUTES.SETTINGS);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when workspace is suspended (DEFAULTED)', async () => {
|
||||
@@ -1204,7 +1196,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
// Should redirect to WORKSPACE_SUSPENDED, not ONBOARDING
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
await assertRedirectsTo(ROUTES.WORKSPACE_SUSPENDED);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when workspace is access restricted (TERMINATED)', async () => {
|
||||
@@ -1231,7 +1223,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
// Should redirect to WORKSPACE_ACCESS_RESTRICTED, not ONBOARDING
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
await assertRedirectsTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when workspace is access restricted (EXPIRED)', async () => {
|
||||
@@ -1257,7 +1249,7 @@ describe('PrivateRoute', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
await assertRedirectsTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1299,7 +1291,7 @@ describe('PrivateRoute', () => {
|
||||
},
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
assertStaysOnRoute(ROUTES.GET_STARTED);
|
||||
});
|
||||
|
||||
it('should not redirect when on GET_STARTED and ONBOARDING_V3 feature flag is not present', () => {
|
||||
@@ -1311,7 +1303,7 @@ describe('PrivateRoute', () => {
|
||||
},
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
assertStaysOnRoute(ROUTES.GET_STARTED);
|
||||
});
|
||||
|
||||
it('should not redirect when on different route even if ONBOARDING_V3 is active', () => {
|
||||
@@ -1331,7 +1323,7 @@ describe('PrivateRoute', () => {
|
||||
},
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1347,7 +1339,7 @@ describe('PrivateRoute', () => {
|
||||
},
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
|
||||
it('should not fetch users when org data is not available', () => {
|
||||
@@ -1390,9 +1382,7 @@ describe('PrivateRoute', () => {
|
||||
},
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_SUSPENDED);
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1433,22 +1423,40 @@ describe('PrivateRoute', () => {
|
||||
await assertRedirectsTo(ROUTES.UN_AUTHORIZED);
|
||||
});
|
||||
|
||||
it('should allow all roles to access /services route', () => {
|
||||
const roles = [USER_ROLES.ADMIN, USER_ROLES.EDITOR, USER_ROLES.VIEWER];
|
||||
|
||||
roles.forEach((role) => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
renderPrivateRoute({
|
||||
initialRoute: ROUTES.APPLICATION,
|
||||
appContext: {
|
||||
isLoggedIn: true,
|
||||
user: createMockUser({ role: role as ROLES }),
|
||||
},
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.UN_AUTHORIZED);
|
||||
it('should allow ADMIN to access /services route', () => {
|
||||
renderPrivateRoute({
|
||||
initialRoute: ROUTES.APPLICATION,
|
||||
appContext: {
|
||||
isLoggedIn: true,
|
||||
user: createMockUser({ role: USER_ROLES.ADMIN as ROLES }),
|
||||
},
|
||||
});
|
||||
|
||||
assertStaysOnRoute(ROUTES.APPLICATION);
|
||||
});
|
||||
|
||||
it('should allow EDITOR to access /services route', () => {
|
||||
renderPrivateRoute({
|
||||
initialRoute: ROUTES.APPLICATION,
|
||||
appContext: {
|
||||
isLoggedIn: true,
|
||||
user: createMockUser({ role: USER_ROLES.EDITOR as ROLES }),
|
||||
},
|
||||
});
|
||||
|
||||
assertStaysOnRoute(ROUTES.APPLICATION);
|
||||
});
|
||||
|
||||
it('should allow VIEWER to access /services route', () => {
|
||||
renderPrivateRoute({
|
||||
initialRoute: ROUTES.APPLICATION,
|
||||
appContext: {
|
||||
isLoggedIn: true,
|
||||
user: createMockUser({ role: USER_ROLES.VIEWER as ROLES }),
|
||||
},
|
||||
});
|
||||
|
||||
assertStaysOnRoute(ROUTES.APPLICATION);
|
||||
});
|
||||
|
||||
it('should redirect VIEWER from /onboarding route (admin only)', async () => {
|
||||
@@ -1478,7 +1486,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
assertRendersChildren();
|
||||
assertDoesNotRedirectTo(ROUTES.UN_AUTHORIZED);
|
||||
assertStaysOnRoute(ROUTES.CHANNELS_NEW);
|
||||
});
|
||||
|
||||
it('should allow EDITOR to access /get-started route', () => {
|
||||
@@ -1490,7 +1498,7 @@ describe('PrivateRoute', () => {
|
||||
},
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.UN_AUTHORIZED);
|
||||
assertStaysOnRoute(ROUTES.GET_STARTED);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import AppLayout from 'container/AppLayout';
|
||||
import Hex from 'crypto-js/enc-hex';
|
||||
import HmacSHA256 from 'crypto-js/hmac-sha256';
|
||||
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { useThemeConfig } from 'hooks/useDarkMode';
|
||||
import { useIsDarkMode, useThemeConfig } from 'hooks/useDarkMode';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { NotificationProvider } from 'hooks/useNotifications';
|
||||
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
||||
@@ -212,6 +212,12 @@ function App(): JSX.Element {
|
||||
activeLicenseFetchError,
|
||||
]);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
useEffect(() => {
|
||||
window.Pylon?.('setTheme', isDarkMode ? 'dark' : 'light');
|
||||
}, [isDarkMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
pathname === ROUTES.ONBOARDING ||
|
||||
|
||||
@@ -33,6 +33,8 @@ import type {
|
||||
DisconnectAccountPathParameters,
|
||||
GetAccount200,
|
||||
GetAccountPathParameters,
|
||||
GetConnectionCredentials200,
|
||||
GetConnectionCredentialsPathParameters,
|
||||
GetService200,
|
||||
GetServicePathParameters,
|
||||
ListAccounts200,
|
||||
@@ -628,6 +630,103 @@ export const useUpdateAccount = <
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint updates a service for the specified cloud provider
|
||||
* @summary Update service
|
||||
*/
|
||||
export const updateService = (
|
||||
{ cloudProvider, id, serviceId }: UpdateServicePathParameters,
|
||||
cloudintegrationtypesUpdatableServiceDTO: BodyType<CloudintegrationtypesUpdatableServiceDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/cloud_integrations/${cloudProvider}/accounts/${id}/services/${serviceId}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: cloudintegrationtypesUpdatableServiceDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateServiceMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateService>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateServicePathParameters;
|
||||
data: BodyType<CloudintegrationtypesUpdatableServiceDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateService>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateServicePathParameters;
|
||||
data: BodyType<CloudintegrationtypesUpdatableServiceDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateService'];
|
||||
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 updateService>>,
|
||||
{
|
||||
pathParams: UpdateServicePathParameters;
|
||||
data: BodyType<CloudintegrationtypesUpdatableServiceDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateService(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateServiceMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateService>>
|
||||
>;
|
||||
export type UpdateServiceMutationBody = BodyType<CloudintegrationtypesUpdatableServiceDTO>;
|
||||
export type UpdateServiceMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update service
|
||||
*/
|
||||
export const useUpdateService = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateService>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateServicePathParameters;
|
||||
data: BodyType<CloudintegrationtypesUpdatableServiceDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateService>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateServicePathParameters;
|
||||
data: BodyType<CloudintegrationtypesUpdatableServiceDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getUpdateServiceMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint is called by the deployed agent to check in
|
||||
* @summary Agent check-in
|
||||
@@ -727,6 +826,114 @@ export const useAgentCheckIn = <
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint retrieves the connection credentials required for integration
|
||||
* @summary Get connection credentials
|
||||
*/
|
||||
export const getConnectionCredentials = (
|
||||
{ cloudProvider }: GetConnectionCredentialsPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetConnectionCredentials200>({
|
||||
url: `/api/v1/cloud_integrations/${cloudProvider}/credentials`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetConnectionCredentialsQueryKey = ({
|
||||
cloudProvider,
|
||||
}: GetConnectionCredentialsPathParameters) => {
|
||||
return [`/api/v1/cloud_integrations/${cloudProvider}/credentials`] as const;
|
||||
};
|
||||
|
||||
export const getGetConnectionCredentialsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getConnectionCredentials>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ cloudProvider }: GetConnectionCredentialsPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getConnectionCredentials>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ??
|
||||
getGetConnectionCredentialsQueryKey({ cloudProvider });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getConnectionCredentials>>
|
||||
> = ({ signal }) => getConnectionCredentials({ cloudProvider }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!cloudProvider,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getConnectionCredentials>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetConnectionCredentialsQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getConnectionCredentials>>
|
||||
>;
|
||||
export type GetConnectionCredentialsQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get connection credentials
|
||||
*/
|
||||
|
||||
export function useGetConnectionCredentials<
|
||||
TData = Awaited<ReturnType<typeof getConnectionCredentials>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ cloudProvider }: GetConnectionCredentialsPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getConnectionCredentials>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetConnectionCredentialsQueryOptions(
|
||||
{ cloudProvider },
|
||||
options,
|
||||
);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get connection credentials
|
||||
*/
|
||||
export const invalidateGetConnectionCredentials = async (
|
||||
queryClient: QueryClient,
|
||||
{ cloudProvider }: GetConnectionCredentialsPathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetConnectionCredentialsQueryKey({ cloudProvider }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint lists the services metadata for the specified cloud provider
|
||||
* @summary List services metadata
|
||||
@@ -941,101 +1148,3 @@ export const invalidateGetService = async (
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint updates a service for the specified cloud provider
|
||||
* @summary Update service
|
||||
*/
|
||||
export const updateService = (
|
||||
{ cloudProvider, serviceId }: UpdateServicePathParameters,
|
||||
cloudintegrationtypesUpdatableServiceDTO: BodyType<CloudintegrationtypesUpdatableServiceDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: cloudintegrationtypesUpdatableServiceDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateServiceMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateService>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateServicePathParameters;
|
||||
data: BodyType<CloudintegrationtypesUpdatableServiceDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateService>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateServicePathParameters;
|
||||
data: BodyType<CloudintegrationtypesUpdatableServiceDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateService'];
|
||||
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 updateService>>,
|
||||
{
|
||||
pathParams: UpdateServicePathParameters;
|
||||
data: BodyType<CloudintegrationtypesUpdatableServiceDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateService(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateServiceMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateService>>
|
||||
>;
|
||||
export type UpdateServiceMutationBody = BodyType<CloudintegrationtypesUpdatableServiceDTO>;
|
||||
export type UpdateServiceMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update service
|
||||
*/
|
||||
export const useUpdateService = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateService>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateServicePathParameters;
|
||||
data: BodyType<CloudintegrationtypesUpdatableServiceDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateService>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateServicePathParameters;
|
||||
data: BodyType<CloudintegrationtypesUpdatableServiceDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getUpdateServiceMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
|
||||
@@ -425,39 +425,6 @@ export interface AuthtypesSessionContextDTO {
|
||||
orgs?: AuthtypesOrgSessionContextDTO[] | null;
|
||||
}
|
||||
|
||||
export interface AuthtypesStorableRoleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface AuthtypesTransactionDTO {
|
||||
object: AuthtypesObjectDTO;
|
||||
/**
|
||||
@@ -475,25 +442,25 @@ export interface AuthtypesUserRoleDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
createdAt: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
role?: AuthtypesStorableRoleDTO;
|
||||
role: AuthtypesRoleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
roleId?: string;
|
||||
roleId: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
updatedAt: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
userId?: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface AuthtypesUserWithRolesDTO {
|
||||
@@ -550,12 +517,12 @@ export type CloudintegrationtypesAWSCollectionStrategyDTOS3Buckets = {
|
||||
};
|
||||
|
||||
export interface CloudintegrationtypesAWSCollectionStrategyDTO {
|
||||
aws_logs?: CloudintegrationtypesAWSLogsStrategyDTO;
|
||||
aws_metrics?: CloudintegrationtypesAWSMetricsStrategyDTO;
|
||||
logs?: CloudintegrationtypesAWSLogsStrategyDTO;
|
||||
metrics?: CloudintegrationtypesAWSMetricsStrategyDTO;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
s3_buckets?: CloudintegrationtypesAWSCollectionStrategyDTOS3Buckets;
|
||||
s3Buckets?: CloudintegrationtypesAWSCollectionStrategyDTOS3Buckets;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesAWSConnectionArtifactDTO {
|
||||
@@ -588,11 +555,11 @@ export type CloudintegrationtypesAWSLogsStrategyDTOCloudwatchLogsSubscriptionsIt
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
filter_pattern?: string;
|
||||
filterPattern?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
log_group_name_prefix?: string;
|
||||
logGroupNamePrefix?: string;
|
||||
};
|
||||
|
||||
export interface CloudintegrationtypesAWSLogsStrategyDTO {
|
||||
@@ -600,7 +567,7 @@ export interface CloudintegrationtypesAWSLogsStrategyDTO {
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
cloudwatch_logs_subscriptions?:
|
||||
cloudwatchLogsSubscriptions?:
|
||||
| CloudintegrationtypesAWSLogsStrategyDTOCloudwatchLogsSubscriptionsItem[]
|
||||
| null;
|
||||
}
|
||||
@@ -621,7 +588,7 @@ export interface CloudintegrationtypesAWSMetricsStrategyDTO {
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
cloudwatch_metric_stream_filters?:
|
||||
cloudwatchMetricStreamFilters?:
|
||||
| CloudintegrationtypesAWSMetricsStrategyDTOCloudwatchMetricStreamFiltersItem[]
|
||||
| null;
|
||||
}
|
||||
@@ -643,7 +610,7 @@ export interface CloudintegrationtypesAWSServiceLogsConfigDTO {
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
s3_buckets?: CloudintegrationtypesAWSServiceLogsConfigDTOS3Buckets;
|
||||
s3Buckets?: CloudintegrationtypesAWSServiceLogsConfigDTOS3Buckets;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesAWSServiceMetricsConfigDTO {
|
||||
@@ -726,6 +693,32 @@ export interface CloudintegrationtypesAssetsDTO {
|
||||
dashboards?: CloudintegrationtypesDashboardDTO[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type CloudintegrationtypesCloudIntegrationServiceDTO = {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
cloudIntegrationId?: string;
|
||||
config?: CloudintegrationtypesServiceConfigDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
type?: CloudintegrationtypesServiceIDDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
} | null;
|
||||
|
||||
export interface CloudintegrationtypesCollectedLogAttributeDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -769,6 +762,11 @@ export interface CloudintegrationtypesConnectionArtifactDTO {
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesConnectionArtifactRequestDTO {
|
||||
config: CloudintegrationtypesConnectionArtifactRequestConfigDTO;
|
||||
credentials: CloudintegrationtypesSignozCredentialsDTO;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesConnectionArtifactRequestConfigDTO {
|
||||
aws: CloudintegrationtypesAWSConnectionArtifactRequestDTO;
|
||||
}
|
||||
|
||||
@@ -864,9 +862,68 @@ export type CloudintegrationtypesIntegrationConfigDTO = {
|
||||
* @type array
|
||||
*/
|
||||
enabled_regions: string[];
|
||||
telemetry: CloudintegrationtypesAWSCollectionStrategyDTO;
|
||||
telemetry: CloudintegrationtypesOldAWSCollectionStrategyDTO;
|
||||
} | null;
|
||||
|
||||
export type CloudintegrationtypesOldAWSCollectionStrategyDTOS3Buckets = {
|
||||
[key: string]: string[];
|
||||
};
|
||||
|
||||
export interface CloudintegrationtypesOldAWSCollectionStrategyDTO {
|
||||
aws_logs?: CloudintegrationtypesOldAWSLogsStrategyDTO;
|
||||
aws_metrics?: CloudintegrationtypesOldAWSMetricsStrategyDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
provider?: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
s3_buckets?: CloudintegrationtypesOldAWSCollectionStrategyDTOS3Buckets;
|
||||
}
|
||||
|
||||
export type CloudintegrationtypesOldAWSLogsStrategyDTOCloudwatchLogsSubscriptionsItem = {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
filter_pattern?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
log_group_name_prefix?: string;
|
||||
};
|
||||
|
||||
export interface CloudintegrationtypesOldAWSLogsStrategyDTO {
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
cloudwatch_logs_subscriptions?:
|
||||
| CloudintegrationtypesOldAWSLogsStrategyDTOCloudwatchLogsSubscriptionsItem[]
|
||||
| null;
|
||||
}
|
||||
|
||||
export type CloudintegrationtypesOldAWSMetricsStrategyDTOCloudwatchMetricStreamFiltersItem = {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
MetricNames?: string[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
Namespace?: string;
|
||||
};
|
||||
|
||||
export interface CloudintegrationtypesOldAWSMetricsStrategyDTO {
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
cloudwatch_metric_stream_filters?:
|
||||
| CloudintegrationtypesOldAWSMetricsStrategyDTOCloudwatchMetricStreamFiltersItem[]
|
||||
| null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
@@ -904,6 +961,7 @@ export interface CloudintegrationtypesProviderIntegrationConfigDTO {
|
||||
|
||||
export interface CloudintegrationtypesServiceDTO {
|
||||
assets: CloudintegrationtypesAssetsDTO;
|
||||
cloudIntegrationService: CloudintegrationtypesCloudIntegrationServiceDTO;
|
||||
dataCollected: CloudintegrationtypesDataCollectedDTO;
|
||||
/**
|
||||
* @type string
|
||||
@@ -917,8 +975,7 @@ export interface CloudintegrationtypesServiceDTO {
|
||||
* @type string
|
||||
*/
|
||||
overview: string;
|
||||
serviceConfig?: CloudintegrationtypesServiceConfigDTO;
|
||||
supported_signals: CloudintegrationtypesSupportedSignalsDTO;
|
||||
supportedSignals: CloudintegrationtypesSupportedSignalsDTO;
|
||||
telemetryCollectionStrategy: CloudintegrationtypesCollectionStrategyDTO;
|
||||
/**
|
||||
* @type string
|
||||
@@ -930,6 +987,21 @@ export interface CloudintegrationtypesServiceConfigDTO {
|
||||
aws: CloudintegrationtypesAWSServiceConfigDTO;
|
||||
}
|
||||
|
||||
export enum CloudintegrationtypesServiceIDDTO {
|
||||
alb = 'alb',
|
||||
'api-gateway' = 'api-gateway',
|
||||
dynamodb = 'dynamodb',
|
||||
ec2 = 'ec2',
|
||||
ecs = 'ecs',
|
||||
eks = 'eks',
|
||||
elasticache = 'elasticache',
|
||||
lambda = 'lambda',
|
||||
msk = 'msk',
|
||||
rds = 'rds',
|
||||
s3sync = 's3sync',
|
||||
sns = 'sns',
|
||||
sqs = 'sqs',
|
||||
}
|
||||
export interface CloudintegrationtypesServiceMetadataDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
@@ -949,6 +1021,25 @@ export interface CloudintegrationtypesServiceMetadataDTO {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesSignozCredentialsDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
ingestionKey: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
ingestionUrl: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
sigNozApiKey: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
sigNozApiURL: string;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesSupportedSignalsDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
@@ -3532,6 +3623,11 @@ export type UpdateAccountPathParameters = {
|
||||
cloudProvider: string;
|
||||
id: string;
|
||||
};
|
||||
export type UpdateServicePathParameters = {
|
||||
cloudProvider: string;
|
||||
id: string;
|
||||
serviceId: string;
|
||||
};
|
||||
export type AgentCheckInPathParameters = {
|
||||
cloudProvider: string;
|
||||
};
|
||||
@@ -3543,6 +3639,17 @@ export type AgentCheckIn200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetConnectionCredentialsPathParameters = {
|
||||
cloudProvider: string;
|
||||
};
|
||||
export type GetConnectionCredentials200 = {
|
||||
data: CloudintegrationtypesSignozCredentialsDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListServicesMetadataPathParameters = {
|
||||
cloudProvider: string;
|
||||
};
|
||||
@@ -3566,10 +3673,6 @@ export type GetService200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateServicePathParameters = {
|
||||
cloudProvider: string;
|
||||
serviceId: string;
|
||||
};
|
||||
export type CreateSessionByGoogleCallback303 = {
|
||||
data: AuthtypesGettableTokenDTO;
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,9 @@ export interface HostListPayload {
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
} | null;
|
||||
start?: number;
|
||||
end?: number;
|
||||
}
|
||||
|
||||
export interface TimeSeriesValue {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { UnderscoreToDotMap } from 'api/utils';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { K8sBaseFilters } from '../Base/K8sBaseList';
|
||||
import { UnderscoreToDotMap } from '../utils';
|
||||
|
||||
export interface K8sClustersListPayload {
|
||||
filters: TagFilter;
|
||||
@@ -19,7 +18,7 @@ export interface K8sClustersListPayload {
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sClusterData {
|
||||
export interface K8sClustersData {
|
||||
clusterUID: string;
|
||||
cpuUsage: number;
|
||||
cpuAllocatable: number;
|
||||
@@ -35,7 +34,7 @@ export interface K8sClustersListResponse {
|
||||
status: string;
|
||||
data: {
|
||||
type: string;
|
||||
records: K8sClusterData[];
|
||||
records: K8sClustersData[];
|
||||
groups: null;
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
@@ -50,7 +49,7 @@ export const clustersMetaMap = [
|
||||
|
||||
export function mapClustersMeta(
|
||||
raw: Record<string, unknown>,
|
||||
): K8sClusterData['meta'] {
|
||||
): K8sClustersData['meta'] {
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
clustersMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
@@ -58,11 +57,11 @@ export function mapClustersMeta(
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sClusterData['meta'];
|
||||
return out as K8sClustersData['meta'];
|
||||
}
|
||||
|
||||
export const getK8sClustersList = async (
|
||||
props: K8sBaseFilters,
|
||||
props: K8sClustersListPayload,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
@@ -74,7 +73,7 @@ export const getK8sClustersList = async (
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters?.items.reduce<typeof props.filters.items>(
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
@@ -107,6 +106,7 @@ export const getK8sClustersList = async (
|
||||
});
|
||||
const payload: K8sClustersListResponse = response.data;
|
||||
|
||||
// one-liner meta mapping
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapClustersMeta(record.meta as Record<string, unknown>),
|
||||
@@ -1,10 +1,22 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { UnderscoreToDotMap } from 'api/utils';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { K8sBaseFilters } from '../Base/K8sBaseList';
|
||||
import { UnderscoreToDotMap } from '../utils';
|
||||
|
||||
export interface K8sDaemonSetsListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sDaemonSetsData {
|
||||
daemonSetName: string;
|
||||
@@ -56,19 +68,20 @@ export function mapDaemonSetsMeta(
|
||||
}
|
||||
|
||||
export const getK8sDaemonSetsList = async (
|
||||
props: K8sBaseFilters,
|
||||
props: K8sDaemonSetsListPayload,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sDaemonSetsListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
// filter prep (unchanged)…
|
||||
const requestProps =
|
||||
dotMetricsEnabled && Array.isArray(props.filters?.items)
|
||||
? {
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters?.items.reduce<typeof props.filters.items>(
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
@@ -101,6 +114,7 @@ export const getK8sDaemonSetsList = async (
|
||||
});
|
||||
const payload: K8sDaemonSetsListResponse = response.data;
|
||||
|
||||
// single-line meta mapping
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapDaemonSetsMeta(record.meta as Record<string, unknown>),
|
||||
@@ -1,12 +1,11 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { UnderscoreToDotMap } from 'api/utils';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { K8sBaseFilters } from '../Base/K8sBaseList';
|
||||
import { UnderscoreToDotMap } from '../utils';
|
||||
|
||||
export interface K8sDeploymentsListPayload {
|
||||
filters: TagFilter;
|
||||
@@ -69,7 +68,7 @@ export function mapDeploymentsMeta(
|
||||
}
|
||||
|
||||
export const getK8sDeploymentsList = async (
|
||||
props: K8sBaseFilters,
|
||||
props: K8sDeploymentsListPayload,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
@@ -81,7 +80,7 @@ export const getK8sDeploymentsList = async (
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters?.items.reduce<typeof props.filters.items>(
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
@@ -114,6 +113,7 @@ export const getK8sDeploymentsList = async (
|
||||
});
|
||||
const payload: K8sDeploymentsListResponse = response.data;
|
||||
|
||||
// single-line mapping
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapDeploymentsMeta(record.meta as Record<string, unknown>),
|
||||
@@ -1,10 +1,22 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { UnderscoreToDotMap } from 'api/utils';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { K8sBaseFilters } from '../Base/K8sBaseList';
|
||||
import { UnderscoreToDotMap } from '../utils';
|
||||
|
||||
export interface K8sJobsListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sJobsData {
|
||||
jobName: string;
|
||||
@@ -56,7 +68,7 @@ export function mapJobsMeta(raw: Record<string, unknown>): K8sJobsData['meta'] {
|
||||
}
|
||||
|
||||
export const getK8sJobsList = async (
|
||||
props: K8sBaseFilters,
|
||||
props: K8sJobsListPayload,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
@@ -68,7 +80,7 @@ export const getK8sJobsList = async (
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters?.items.reduce<typeof props.filters.items>(
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
@@ -101,6 +113,7 @@ export const getK8sJobsList = async (
|
||||
});
|
||||
const payload: K8sJobsListResponse = response.data;
|
||||
|
||||
// one-liner meta mapping
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapJobsMeta(record.meta as Record<string, unknown>),
|
||||
@@ -1,12 +1,11 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { UnderscoreToDotMap } from 'api/utils';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { K8sBaseFilters } from '../Base/K8sBaseList';
|
||||
import { UnderscoreToDotMap } from '../utils';
|
||||
|
||||
export interface K8sNamespacesListPayload {
|
||||
filters: TagFilter;
|
||||
@@ -60,7 +59,7 @@ export function mapNamespacesMeta(
|
||||
}
|
||||
|
||||
export const getK8sNamespacesList = async (
|
||||
props: K8sBaseFilters,
|
||||
props: K8sNamespacesListPayload,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
@@ -72,7 +71,7 @@ export const getK8sNamespacesList = async (
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters?.items.reduce<typeof props.filters.items>(
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
@@ -0,0 +1,127 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { UnderscoreToDotMap } from '../utils';
|
||||
|
||||
export interface K8sNodesListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sNodesData {
|
||||
nodeUID: string;
|
||||
nodeCPUUsage: number;
|
||||
nodeCPUAllocatable: number;
|
||||
nodeMemoryUsage: number;
|
||||
nodeMemoryAllocatable: number;
|
||||
meta: {
|
||||
k8s_node_name: string;
|
||||
k8s_node_uid: string;
|
||||
k8s_cluster_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sNodesListResponse {
|
||||
status: string;
|
||||
data: {
|
||||
type: string;
|
||||
records: K8sNodesData[];
|
||||
groups: null;
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingK8SAgentMetrics: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const nodesMetaMap = [
|
||||
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
|
||||
{ dot: 'k8s.node.uid', under: 'k8s_node_uid' },
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
] as const;
|
||||
|
||||
export function mapNodesMeta(
|
||||
raw: Record<string, unknown>,
|
||||
): K8sNodesData['meta'] {
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
nodesMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
const v = raw[dot];
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sNodesData['meta'];
|
||||
}
|
||||
|
||||
export const getK8sNodesList = async (
|
||||
props: K8sNodesListPayload,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sNodesListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const requestProps =
|
||||
dotMetricsEnabled && Array.isArray(props.filters?.items)
|
||||
? {
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
}
|
||||
if (
|
||||
item.key &&
|
||||
typeof item.key === 'object' &&
|
||||
'key' in item.key &&
|
||||
typeof item.key.key === 'string'
|
||||
) {
|
||||
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
|
||||
acc.push({
|
||||
...item,
|
||||
key: { ...item.key, key: mappedKey },
|
||||
});
|
||||
} else {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
|
||||
const response = await axios.post('/nodes/list', requestProps, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
const payload: K8sNodesListResponse = response.data;
|
||||
|
||||
// one-liner to map dot→underscore
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapNodesMeta(record.meta as Record<string, unknown>),
|
||||
}));
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload,
|
||||
params: requestProps,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,35 +1,21 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { UnderscoreToDotMap } from 'api/utils';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { K8sBaseFilters } from '../Base/K8sBaseList';
|
||||
import { UnderscoreToDotMap } from '../utils';
|
||||
|
||||
export const podsMetaMap = [
|
||||
{ dot: 'k8s.cronjob.name', under: 'k8s_cronjob_name' },
|
||||
{ dot: 'k8s.daemonset.name', under: 'k8s_daemonset_name' },
|
||||
{ dot: 'k8s.deployment.name', under: 'k8s_deployment_name' },
|
||||
{ dot: 'k8s.job.name', under: 'k8s_job_name' },
|
||||
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
|
||||
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
|
||||
{ dot: 'k8s.pod.name', under: 'k8s_pod_name' },
|
||||
{ dot: 'k8s.pod.uid', under: 'k8s_pod_uid' },
|
||||
{ dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
] as const;
|
||||
|
||||
export function mapPodsMeta(raw: Record<string, unknown>): K8sPodsData['meta'] {
|
||||
// clone everything
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
// overlay only the dot→under mappings
|
||||
podsMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
const v = raw[dot];
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sPodsData['meta'];
|
||||
export interface K8sPodsListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
export interface TimeSeriesValue {
|
||||
@@ -85,9 +71,35 @@ export interface K8sPodsListResponse {
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Remove this method once we move this to OpenAPI
|
||||
export const podsMetaMap = [
|
||||
{ dot: 'k8s.cronjob.name', under: 'k8s_cronjob_name' },
|
||||
{ dot: 'k8s.daemonset.name', under: 'k8s_daemonset_name' },
|
||||
{ dot: 'k8s.deployment.name', under: 'k8s_deployment_name' },
|
||||
{ dot: 'k8s.job.name', under: 'k8s_job_name' },
|
||||
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
|
||||
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
|
||||
{ dot: 'k8s.pod.name', under: 'k8s_pod_name' },
|
||||
{ dot: 'k8s.pod.uid', under: 'k8s_pod_uid' },
|
||||
{ dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
] as const;
|
||||
|
||||
export function mapPodsMeta(raw: Record<string, unknown>): K8sPodsData['meta'] {
|
||||
// clone everything
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
// overlay only the dot→under mappings
|
||||
podsMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
const v = raw[dot];
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sPodsData['meta'];
|
||||
}
|
||||
|
||||
// getK8sPodsList
|
||||
export const getK8sPodsList = async (
|
||||
props: K8sBaseFilters,
|
||||
props: K8sPodsListPayload,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
@@ -99,7 +111,7 @@ export const getK8sPodsList = async (
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters?.items.reduce<typeof props.filters.items>(
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
@@ -1,10 +1,22 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { UnderscoreToDotMap } from 'api/utils';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { K8sBaseFilters } from '../Base/K8sBaseList';
|
||||
import { UnderscoreToDotMap } from '../utils';
|
||||
|
||||
export interface K8sVolumesListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sVolumesData {
|
||||
persistentVolumeClaimName: string;
|
||||
@@ -56,8 +68,10 @@ export const volumesMetaMap: Array<{
|
||||
export function mapVolumesMeta(
|
||||
rawMeta: Record<string, unknown>,
|
||||
): K8sVolumesData['meta'] {
|
||||
// start with everything that was already there
|
||||
const out: Record<string, unknown> = { ...rawMeta };
|
||||
|
||||
// for each dot→under rule, if the raw has the dot, overwrite the underscore
|
||||
volumesMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in rawMeta) {
|
||||
const val = rawMeta[dot];
|
||||
@@ -69,47 +83,42 @@ export function mapVolumesMeta(
|
||||
}
|
||||
|
||||
export const getK8sVolumesList = async (
|
||||
props: K8sBaseFilters,
|
||||
props: K8sVolumesListPayload,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sVolumesListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const { orderBy, ...rest } = props;
|
||||
const basePayload = {
|
||||
...rest,
|
||||
filters: props.filters ?? { items: [], op: 'and' },
|
||||
...(orderBy != null ? { orderBy } : {}),
|
||||
};
|
||||
|
||||
// Prepare filters
|
||||
const requestProps =
|
||||
dotMetricsEnabled && Array.isArray(basePayload.filters?.items)
|
||||
dotMetricsEnabled && Array.isArray(props.filters?.items)
|
||||
? {
|
||||
...basePayload,
|
||||
...props,
|
||||
filters: {
|
||||
...basePayload.filters,
|
||||
items: basePayload.filters.items.reduce<
|
||||
typeof basePayload.filters.items
|
||||
>((acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
...props.filters,
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
}
|
||||
if (
|
||||
item.key &&
|
||||
typeof item.key === 'object' &&
|
||||
'key' in item.key &&
|
||||
typeof item.key.key === 'string'
|
||||
) {
|
||||
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
|
||||
acc.push({ ...item, key: { ...item.key, key: mappedKey } });
|
||||
} else {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
if (
|
||||
item.key &&
|
||||
typeof item.key === 'object' &&
|
||||
'key' in item.key &&
|
||||
typeof item.key.key === 'string'
|
||||
) {
|
||||
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
|
||||
acc.push({ ...item, key: { ...item.key, key: mappedKey } });
|
||||
} else {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
}, [] as typeof basePayload.filters.items),
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: basePayload;
|
||||
: props;
|
||||
|
||||
const response = await axios.post('/pvcs/list', requestProps, {
|
||||
signal,
|
||||
@@ -1,12 +1,11 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { UnderscoreToDotMap } from 'api/utils';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { K8sBaseFilters } from '../Base/K8sBaseList';
|
||||
import { UnderscoreToDotMap } from '../utils';
|
||||
|
||||
export interface K8sStatefulSetsListPayload {
|
||||
filters: TagFilter;
|
||||
@@ -67,35 +66,39 @@ export function mapStatefulSetsMeta(
|
||||
}
|
||||
|
||||
export const getK8sStatefulSetsList = async (
|
||||
props: K8sBaseFilters,
|
||||
props: K8sStatefulSetsListPayload,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sStatefulSetsListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
// Prepare filters
|
||||
const requestProps =
|
||||
dotMetricsEnabled && props.filters && Array.isArray(props.filters.items)
|
||||
dotMetricsEnabled && Array.isArray(props.filters?.items)
|
||||
? {
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters.items.reduce<TagFilter['items']>((acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
}
|
||||
if (
|
||||
item.key &&
|
||||
typeof item.key === 'object' &&
|
||||
'key' in item.key &&
|
||||
typeof item.key.key === 'string'
|
||||
) {
|
||||
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
|
||||
acc.push({ ...item, key: { ...item.key, key: mappedKey } });
|
||||
} else {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
if (
|
||||
item.key &&
|
||||
typeof item.key === 'object' &&
|
||||
'key' in item.key &&
|
||||
typeof item.key.key === 'string'
|
||||
) {
|
||||
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
|
||||
acc.push({ ...item, key: { ...item.key, key: mappedKey } });
|
||||
} else {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
}, [] as TagFilter['items']),
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
@@ -106,6 +109,7 @@ export const getK8sStatefulSetsList = async (
|
||||
});
|
||||
const payload: K8sStatefulSetsListResponse = response.data;
|
||||
|
||||
// apply our helper
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapStatefulSetsMeta(record.meta as Record<string, unknown>),
|
||||
@@ -1,26 +0,0 @@
|
||||
import { ApiV2Instance as axios } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/user/editOrg';
|
||||
|
||||
const editOrg = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`/orgs/me`, {
|
||||
displayName: props.displayName,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 204,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default editOrg;
|
||||
@@ -1,28 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/user/getOrganization';
|
||||
|
||||
const getOrganization = async (
|
||||
token?: string,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`/org`, {
|
||||
headers: {
|
||||
Authorization: `bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getOrganization;
|
||||
@@ -1,21 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { UserResponse } from 'types/api/user/getUser';
|
||||
import { PayloadProps } from 'types/api/user/getUsers';
|
||||
|
||||
const getAll = async (): Promise<SuccessResponseV2<UserResponse[]>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/user`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default getAll;
|
||||
@@ -1,22 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props, UserResponse } from 'types/api/user/getUser';
|
||||
|
||||
const getUser = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<UserResponse>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/user/${props.userId}`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default getUser;
|
||||
@@ -1,23 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { Props } from 'types/api/user/editUser';
|
||||
|
||||
const update = async (props: Props): Promise<SuccessResponseV2<null>> => {
|
||||
try {
|
||||
const response = await axios.put(`/user/${props.userId}`, {
|
||||
displayName: props.displayName,
|
||||
role: props.role,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: null,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default update;
|
||||
@@ -1,20 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, UserResponse } from 'types/api/user/getUser';
|
||||
|
||||
const get = async (): Promise<SuccessResponseV2<UserResponse>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/user/me`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default get;
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Button } from '@signozhq/button';
|
||||
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
|
||||
import { Trash2, X } from '@signozhq/icons';
|
||||
import { MemberRow } from 'components/MembersTable/MembersTable';
|
||||
|
||||
interface DeleteMemberDialogProps {
|
||||
open: boolean;
|
||||
isInvited: boolean;
|
||||
member: MemberRow | null;
|
||||
isDeleting: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
function DeleteMemberDialog({
|
||||
open,
|
||||
isInvited,
|
||||
member,
|
||||
isDeleting,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: DeleteMemberDialogProps): JSX.Element {
|
||||
const title = isInvited ? 'Revoke Invite' : 'Delete Member';
|
||||
|
||||
const body = isInvited ? (
|
||||
<>
|
||||
Are you sure you want to revoke the invite for{' '}
|
||||
<strong>{member?.email}</strong>? They will no longer be able to join the
|
||||
workspace using this invite.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Are you sure you want to delete{' '}
|
||||
<strong>{member?.name || member?.email}</strong>? This will remove their
|
||||
access to the workspace.
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<DialogWrapper
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title={title}
|
||||
width="narrow"
|
||||
className="alert-dialog delete-dialog"
|
||||
showCloseButton={false}
|
||||
disableOutsideClick={false}
|
||||
>
|
||||
<p className="delete-dialog__body">{body}</p>
|
||||
|
||||
<DialogFooter className="delete-dialog__footer">
|
||||
<Button variant="solid" color="secondary" size="sm" onClick={onClose}>
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
size="sm"
|
||||
disabled={isDeleting}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{isDeleting ? 'Processing...' : title}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeleteMemberDialog;
|
||||
@@ -45,8 +45,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 32px;
|
||||
padding: 0 var(--padding-2);
|
||||
min-height: 32px;
|
||||
padding: var(--padding-1) var(--padding-2);
|
||||
border-radius: 2px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--border);
|
||||
@@ -57,6 +57,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__disabled-roles {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-2);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__email-text {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
@@ -76,35 +83,6 @@
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&__role-select {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
|
||||
.ant-select-selector {
|
||||
background-color: var(--l2-background) !important;
|
||||
border-color: var(--border) !important;
|
||||
border-radius: 2px;
|
||||
padding: 0 var(--padding-2) !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--l1-foreground);
|
||||
line-height: 32px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
&:not(.ant-select-disabled):hover .ant-select-selector {
|
||||
border-color: var(--foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -168,6 +146,10 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__tooltip-wrapper {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
&__footer-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -2,38 +2,65 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Badge } from '@signozhq/badge';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
|
||||
import { DrawerWrapper } from '@signozhq/drawer';
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
Copy,
|
||||
LockKeyhole,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
X,
|
||||
} from '@signozhq/icons';
|
||||
import { LockKeyhole, RefreshCw, Trash2, X } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { Select } from 'antd';
|
||||
import { Skeleton, Tooltip } from 'antd';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
getResetPasswordToken,
|
||||
useDeleteUser,
|
||||
useUpdateUserDeprecated,
|
||||
useGetUser,
|
||||
useUpdateMyUserV2,
|
||||
useUpdateUser,
|
||||
} from 'api/generated/services/users';
|
||||
import { AxiosError } from 'axios';
|
||||
import { MemberRow } from 'components/MembersTable/MembersTable';
|
||||
import RolesSelect, { useRoles } from 'components/RolesSelect';
|
||||
import SaveErrorItem from 'components/ServiceAccountDrawer/SaveErrorItem';
|
||||
import type { SaveError } from 'components/ServiceAccountDrawer/utils';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { MemberStatus } from 'container/MembersSettings/utils';
|
||||
import { capitalize } from 'lodash-es';
|
||||
import {
|
||||
MemberRoleUpdateFailure,
|
||||
useMemberRoleManager,
|
||||
} from 'hooks/member/useMemberRoleManager';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { ROLES } from 'types/roles';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import APIError from 'types/api/error';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import DeleteMemberDialog from './DeleteMemberDialog';
|
||||
import ResetLinkDialog from './ResetLinkDialog';
|
||||
|
||||
import './EditMemberDrawer.styles.scss';
|
||||
|
||||
const ROOT_USER_TOOLTIP = 'This operation is not supported for the root user';
|
||||
const SELF_DELETE_TOOLTIP =
|
||||
'You cannot perform this action on your own account';
|
||||
|
||||
function getDeleteTooltip(
|
||||
isRootUser: boolean,
|
||||
isSelf: boolean,
|
||||
): string | undefined {
|
||||
if (isRootUser) {
|
||||
return ROOT_USER_TOOLTIP;
|
||||
}
|
||||
if (isSelf) {
|
||||
return SELF_DELETE_TOOLTIP;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function toSaveApiError(err: unknown): APIError {
|
||||
return (
|
||||
convertToApiError(err as AxiosError<RenderErrorResponseDTO>) ??
|
||||
toAPIError(err as AxiosError<RenderErrorResponseDTO>)
|
||||
);
|
||||
}
|
||||
|
||||
export interface EditMemberDrawerProps {
|
||||
member: MemberRow | null;
|
||||
open: boolean;
|
||||
@@ -49,9 +76,12 @@ function EditMemberDrawer({
|
||||
onComplete,
|
||||
}: EditMemberDrawerProps): JSX.Element {
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
const { user: currentUser } = useAppContext();
|
||||
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [selectedRole, setSelectedRole] = useState<ROLES>('VIEWER');
|
||||
const [localDisplayName, setLocalDisplayName] = useState('');
|
||||
const [localRole, setLocalRole] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveErrors, setSaveErrors] = useState<SaveError[]>([]);
|
||||
const [isGeneratingLink, setIsGeneratingLink] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [resetLink, setResetLink] = useState<string | null>(null);
|
||||
@@ -60,32 +90,63 @@ function EditMemberDrawer({
|
||||
const [linkType, setLinkType] = useState<'invite' | 'reset' | null>(null);
|
||||
|
||||
const isInvited = member?.status === MemberStatus.Invited;
|
||||
const isSelf = !!member?.id && member.id === currentUser?.id;
|
||||
|
||||
const { mutate: updateUser, isLoading: isSaving } = useUpdateUserDeprecated({
|
||||
mutation: {
|
||||
onSuccess: (): void => {
|
||||
toast.success('Member details updated successfully', { richColors: true });
|
||||
onComplete();
|
||||
onClose();
|
||||
},
|
||||
onError: (err): void => {
|
||||
const errMessage =
|
||||
convertToApiError(
|
||||
err as AxiosError<RenderErrorResponseDTO, unknown> | null,
|
||||
)?.getErrorMessage() || 'An error occurred';
|
||||
toast.error(`Failed to update member details: ${errMessage}`, {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
const {
|
||||
data: fetchedUser,
|
||||
isLoading: isFetchingUser,
|
||||
refetch: refetchUser,
|
||||
} = useGetUser(
|
||||
{ id: member?.id ?? '' },
|
||||
{ query: { enabled: open && !!member?.id } },
|
||||
);
|
||||
|
||||
const isRootUser = !!fetchedUser?.data?.isRoot;
|
||||
|
||||
const {
|
||||
roles: availableRoles,
|
||||
isLoading: rolesLoading,
|
||||
isError: rolesError,
|
||||
error: rolesErrorObj,
|
||||
refetch: refetchRoles,
|
||||
} = useRoles();
|
||||
|
||||
const { fetchedRoleIds, applyDiff } = useMemberRoleManager(
|
||||
member?.id ?? '',
|
||||
open && !!member?.id,
|
||||
);
|
||||
|
||||
const fetchedDisplayName =
|
||||
fetchedUser?.data?.displayName ?? member?.name ?? '';
|
||||
const fetchedUserId = fetchedUser?.data?.id;
|
||||
const fetchedUserDisplayName = fetchedUser?.data?.displayName;
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchedUserId) {
|
||||
setLocalDisplayName(fetchedUserDisplayName ?? member?.name ?? '');
|
||||
}
|
||||
setSaveErrors([]);
|
||||
}, [fetchedUserId, fetchedUserDisplayName, member?.name]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalRole(fetchedRoleIds[0] ?? '');
|
||||
}, [fetchedRoleIds]);
|
||||
|
||||
const isDirty =
|
||||
member !== null &&
|
||||
fetchedUser != null &&
|
||||
(localDisplayName !== fetchedDisplayName ||
|
||||
localRole !== (fetchedRoleIds[0] ?? ''));
|
||||
|
||||
const { mutateAsync: updateMyUser } = useUpdateMyUserV2();
|
||||
const { mutateAsync: updateUser } = useUpdateUser();
|
||||
|
||||
const { mutate: deleteUser, isLoading: isDeleting } = useDeleteUser({
|
||||
mutation: {
|
||||
onSuccess: (): void => {
|
||||
toast.success(
|
||||
isInvited ? 'Invite revoked successfully' : 'Member deleted successfully',
|
||||
{ richColors: true },
|
||||
{ richColors: true, position: 'top-right' },
|
||||
);
|
||||
setShowDeleteConfirm(false);
|
||||
onComplete();
|
||||
@@ -99,53 +160,168 @@ function EditMemberDrawer({
|
||||
const prefix = isInvited
|
||||
? 'Failed to revoke invite'
|
||||
: 'Failed to delete member';
|
||||
toast.error(`${prefix}: ${errMessage}`, { richColors: true });
|
||||
toast.error(`${prefix}: ${errMessage}`, {
|
||||
richColors: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (member) {
|
||||
setDisplayName(member.name ?? '');
|
||||
setSelectedRole(member.role);
|
||||
}
|
||||
}, [member]);
|
||||
|
||||
const isDirty =
|
||||
member !== null &&
|
||||
(displayName !== (member.name ?? '') || selectedRole !== member.role);
|
||||
|
||||
const formatTimestamp = useCallback(
|
||||
(ts: string | null | undefined): string => {
|
||||
if (!ts) {
|
||||
return '—';
|
||||
const makeRoleRetry = useCallback(
|
||||
(
|
||||
context: string,
|
||||
rawRetry: () => Promise<void>,
|
||||
) => async (): Promise<void> => {
|
||||
try {
|
||||
await rawRetry();
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== context));
|
||||
refetchUser();
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
e.context === context ? { ...e, apiError: toSaveApiError(err) } : e,
|
||||
),
|
||||
);
|
||||
}
|
||||
const d = new Date(ts);
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
return '—';
|
||||
}
|
||||
return formatTimezoneAdjustedTimestamp(ts, DATE_TIME_FORMATS.DASH_DATETIME);
|
||||
},
|
||||
[formatTimezoneAdjustedTimestamp],
|
||||
[refetchUser],
|
||||
);
|
||||
|
||||
const handleSave = useCallback((): void => {
|
||||
const retryNameUpdate = useCallback(async (): Promise<void> => {
|
||||
if (!member) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (isSelf) {
|
||||
await updateMyUser({ data: { displayName: localDisplayName } });
|
||||
} else {
|
||||
await updateUser({
|
||||
pathParams: { id: member.id },
|
||||
data: { displayName: localDisplayName },
|
||||
});
|
||||
}
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Name update'));
|
||||
refetchUser();
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
e.context === 'Name update' ? { ...e, apiError: toSaveApiError(err) } : e,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [member, isSelf, localDisplayName, updateMyUser, updateUser, refetchUser]);
|
||||
|
||||
const handleSave = useCallback(async (): Promise<void> => {
|
||||
if (!member || !isDirty) {
|
||||
return;
|
||||
}
|
||||
updateUser({
|
||||
pathParams: { id: member.id },
|
||||
data: { id: member.id, displayName, role: selectedRole },
|
||||
});
|
||||
}, [member, isDirty, displayName, selectedRole, updateUser]);
|
||||
setSaveErrors([]);
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const nameChanged = localDisplayName !== fetchedDisplayName;
|
||||
const rolesChanged = localRole !== (fetchedRoleIds[0] ?? '');
|
||||
|
||||
const namePromise = nameChanged
|
||||
? isSelf
|
||||
? updateMyUser({ data: { displayName: localDisplayName } })
|
||||
: updateUser({
|
||||
pathParams: { id: member.id },
|
||||
data: { displayName: localDisplayName },
|
||||
})
|
||||
: Promise.resolve();
|
||||
|
||||
const [nameResult, rolesResult] = await Promise.allSettled([
|
||||
namePromise,
|
||||
rolesChanged
|
||||
? applyDiff([localRole].filter(Boolean), availableRoles)
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const errors: SaveError[] = [];
|
||||
|
||||
if (nameResult.status === 'rejected') {
|
||||
errors.push({
|
||||
context: 'Name update',
|
||||
apiError: toSaveApiError(nameResult.reason),
|
||||
onRetry: retryNameUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
if (rolesResult.status === 'rejected') {
|
||||
errors.push({
|
||||
context: 'Roles update',
|
||||
apiError: toSaveApiError(rolesResult.reason),
|
||||
onRetry: async (): Promise<void> => {
|
||||
const failures = await applyDiff(
|
||||
[localRole].filter(Boolean),
|
||||
availableRoles,
|
||||
);
|
||||
setSaveErrors((prev) => {
|
||||
const rest = prev.filter((e) => e.context !== 'Roles update');
|
||||
return [
|
||||
...rest,
|
||||
...failures.map((f: MemberRoleUpdateFailure) => {
|
||||
const ctx = `Role '${f.roleName}'`;
|
||||
return {
|
||||
context: ctx,
|
||||
apiError: toSaveApiError(f.error),
|
||||
onRetry: makeRoleRetry(ctx, f.onRetry),
|
||||
};
|
||||
}),
|
||||
];
|
||||
});
|
||||
refetchUser();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
for (const failure of rolesResult.value ?? []) {
|
||||
const context = `Role '${failure.roleName}'`;
|
||||
errors.push({
|
||||
context,
|
||||
apiError: toSaveApiError(failure.error),
|
||||
onRetry: makeRoleRetry(context, failure.onRetry),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
setSaveErrors(errors);
|
||||
} else {
|
||||
toast.success('Member details updated successfully', {
|
||||
richColors: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
onComplete();
|
||||
}
|
||||
|
||||
refetchUser();
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [
|
||||
member,
|
||||
isDirty,
|
||||
isSelf,
|
||||
localDisplayName,
|
||||
localRole,
|
||||
fetchedDisplayName,
|
||||
fetchedRoleIds,
|
||||
updateMyUser,
|
||||
updateUser,
|
||||
applyDiff,
|
||||
availableRoles,
|
||||
refetchUser,
|
||||
retryNameUpdate,
|
||||
makeRoleRetry,
|
||||
onComplete,
|
||||
]);
|
||||
|
||||
const handleDelete = useCallback((): void => {
|
||||
if (!member) {
|
||||
return;
|
||||
}
|
||||
deleteUser({
|
||||
pathParams: { id: member.id },
|
||||
});
|
||||
deleteUser({ pathParams: { id: member.id } });
|
||||
}, [member, deleteUser]);
|
||||
|
||||
const handleGenerateResetLink = useCallback(async (): Promise<void> => {
|
||||
@@ -176,29 +352,28 @@ function EditMemberDrawer({
|
||||
} finally {
|
||||
setIsGeneratingLink(false);
|
||||
}
|
||||
}, [member, isInvited, setLinkType, onClose]);
|
||||
}, [member, isInvited, onClose]);
|
||||
|
||||
const [copyState, copyToClipboard] = useCopyToClipboard();
|
||||
const handleCopyResetLink = useCallback(async (): Promise<void> => {
|
||||
const handleCopyResetLink = useCallback((): void => {
|
||||
if (!resetLink) {
|
||||
return;
|
||||
}
|
||||
copyToClipboard(resetLink);
|
||||
|
||||
setHasCopiedResetLink(true);
|
||||
setTimeout(() => setHasCopiedResetLink(false), 2000);
|
||||
toast.success(
|
||||
const message =
|
||||
linkType === 'invite'
|
||||
? 'Invite link copied to clipboard'
|
||||
: 'Reset link copied to clipboard',
|
||||
{ richColors: true },
|
||||
);
|
||||
: 'Reset link copied to clipboard';
|
||||
toast.success(message, { richColors: true, position: 'top-right' });
|
||||
}, [resetLink, copyToClipboard, linkType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (copyState.error) {
|
||||
toast.error('Failed to copy link', {
|
||||
richColors: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
}, [copyState.error]);
|
||||
@@ -210,102 +385,176 @@ function EditMemberDrawer({
|
||||
|
||||
const joinedOnLabel = isInvited ? 'Invited On' : 'Joined On';
|
||||
|
||||
const drawerContent = (
|
||||
<div className="edit-member-drawer__layout">
|
||||
<div className="edit-member-drawer__body">
|
||||
<div className="edit-member-drawer__field">
|
||||
<label className="edit-member-drawer__label" htmlFor="member-name">
|
||||
Name
|
||||
</label>
|
||||
const formatTimestamp = useCallback(
|
||||
(ts: string | null | undefined): string => {
|
||||
if (!ts) {
|
||||
return '—';
|
||||
}
|
||||
const d = new Date(ts);
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
return '—';
|
||||
}
|
||||
return formatTimezoneAdjustedTimestamp(ts, DATE_TIME_FORMATS.DASH_DATETIME);
|
||||
},
|
||||
[formatTimezoneAdjustedTimestamp],
|
||||
);
|
||||
|
||||
const drawerBody = isFetchingUser ? (
|
||||
<Skeleton active paragraph={{ rows: 6 }} />
|
||||
) : (
|
||||
<>
|
||||
<div className="edit-member-drawer__field">
|
||||
<label className="edit-member-drawer__label" htmlFor="member-name">
|
||||
Name
|
||||
</label>
|
||||
<Tooltip title={isRootUser ? ROOT_USER_TOOLTIP : undefined}>
|
||||
<Input
|
||||
id="member-name"
|
||||
value={displayName}
|
||||
onChange={(e): void => setDisplayName(e.target.value)}
|
||||
value={localDisplayName}
|
||||
onChange={(e): void => {
|
||||
setLocalDisplayName(e.target.value);
|
||||
setSaveErrors((prev) =>
|
||||
prev.filter((err) => err.context !== 'Name update'),
|
||||
);
|
||||
}}
|
||||
className="edit-member-drawer__input"
|
||||
placeholder="Enter name"
|
||||
disabled={isRootUser}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__field">
|
||||
<label className="edit-member-drawer__label" htmlFor="member-email">
|
||||
Email Address
|
||||
</label>
|
||||
<div className="edit-member-drawer__input-wrapper edit-member-drawer__input-wrapper--disabled">
|
||||
<span className="edit-member-drawer__email-text">
|
||||
{member?.email || '—'}
|
||||
</span>
|
||||
<LockKeyhole size={16} className="edit-member-drawer__lock-icon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__field">
|
||||
<label className="edit-member-drawer__label" htmlFor="member-role">
|
||||
Roles
|
||||
</label>
|
||||
<Select
|
||||
id="member-role"
|
||||
value={selectedRole}
|
||||
onChange={(role): void => setSelectedRole(role as ROLES)}
|
||||
className="edit-member-drawer__role-select"
|
||||
suffixIcon={<ChevronDown size={14} />}
|
||||
getPopupContainer={popupContainer}
|
||||
>
|
||||
<Select.Option value="ADMIN">{capitalize('ADMIN')}</Select.Option>
|
||||
<Select.Option value="EDITOR">{capitalize('EDITOR')}</Select.Option>
|
||||
<Select.Option value="VIEWER">{capitalize('VIEWER')}</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__meta">
|
||||
<div className="edit-member-drawer__meta-item">
|
||||
<span className="edit-member-drawer__meta-label">Status</span>
|
||||
{member?.status === MemberStatus.Active ? (
|
||||
<Badge color="forest" variant="outline">
|
||||
ACTIVE
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge color="amber" variant="outline">
|
||||
INVITED
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__meta-item">
|
||||
<span className="edit-member-drawer__meta-label">{joinedOnLabel}</span>
|
||||
<Badge color="vanilla">{formatTimestamp(member?.joinedOn)}</Badge>
|
||||
</div>
|
||||
{!isInvited && (
|
||||
<div className="edit-member-drawer__meta-item">
|
||||
<span className="edit-member-drawer__meta-label">Last Modified</span>
|
||||
<Badge color="vanilla">{formatTimestamp(member?.updatedAt)}</Badge>
|
||||
</div>
|
||||
)}
|
||||
<div className="edit-member-drawer__field">
|
||||
<label className="edit-member-drawer__label" htmlFor="member-email">
|
||||
Email Address
|
||||
</label>
|
||||
<div className="edit-member-drawer__input-wrapper edit-member-drawer__input-wrapper--disabled">
|
||||
<span className="edit-member-drawer__email-text">
|
||||
{member?.email || '—'}
|
||||
</span>
|
||||
<LockKeyhole size={16} className="edit-member-drawer__lock-icon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__field">
|
||||
<label className="edit-member-drawer__label" htmlFor="member-role">
|
||||
Roles
|
||||
</label>
|
||||
{isSelf || isRootUser ? (
|
||||
<Tooltip
|
||||
title={isRootUser ? ROOT_USER_TOOLTIP : 'You cannot modify your own role'}
|
||||
>
|
||||
<div className="edit-member-drawer__input-wrapper edit-member-drawer__input-wrapper--disabled">
|
||||
<div className="edit-member-drawer__disabled-roles">
|
||||
{localRole ? (
|
||||
<Badge color="vanilla">
|
||||
{availableRoles.find((r) => r.id === localRole)?.name ?? localRole}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="edit-member-drawer__email-text">—</span>
|
||||
)}
|
||||
</div>
|
||||
<LockKeyhole size={16} className="edit-member-drawer__lock-icon" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<RolesSelect
|
||||
id="member-role"
|
||||
roles={availableRoles}
|
||||
loading={rolesLoading}
|
||||
isError={rolesError}
|
||||
error={rolesErrorObj}
|
||||
onRefetch={refetchRoles}
|
||||
value={localRole}
|
||||
onChange={(role): void => {
|
||||
setLocalRole(role);
|
||||
setSaveErrors((prev) =>
|
||||
prev.filter(
|
||||
(err) =>
|
||||
err.context !== 'Roles update' && !err.context.startsWith("Role '"),
|
||||
),
|
||||
);
|
||||
}}
|
||||
placeholder="Select role"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__meta">
|
||||
<div className="edit-member-drawer__meta-item">
|
||||
<span className="edit-member-drawer__meta-label">Status</span>
|
||||
{member?.status === MemberStatus.Active ? (
|
||||
<Badge color="forest" variant="outline">
|
||||
ACTIVE
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge color="amber" variant="outline">
|
||||
INVITED
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__meta-item">
|
||||
<span className="edit-member-drawer__meta-label">{joinedOnLabel}</span>
|
||||
<Badge color="vanilla">{formatTimestamp(member?.joinedOn)}</Badge>
|
||||
</div>
|
||||
{!isInvited && (
|
||||
<div className="edit-member-drawer__meta-item">
|
||||
<span className="edit-member-drawer__meta-label">Last Modified</span>
|
||||
<Badge color="vanilla">{formatTimestamp(member?.updatedAt)}</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{saveErrors.length > 0 && (
|
||||
<div className="edit-member-drawer__save-errors">
|
||||
{saveErrors.map((e) => (
|
||||
<SaveErrorItem
|
||||
key={e.context}
|
||||
context={e.context}
|
||||
apiError={e.apiError}
|
||||
onRetry={e.onRetry}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const drawerContent = (
|
||||
<div className="edit-member-drawer__layout">
|
||||
<div className="edit-member-drawer__body">{drawerBody}</div>
|
||||
|
||||
<div className="edit-member-drawer__footer">
|
||||
<div className="edit-member-drawer__footer-left">
|
||||
<Button
|
||||
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--danger"
|
||||
onClick={(): void => setShowDeleteConfirm(true)}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{isInvited ? 'Revoke Invite' : 'Delete Member'}
|
||||
</Button>
|
||||
<Tooltip title={getDeleteTooltip(isRootUser, isSelf)}>
|
||||
<span className="edit-member-drawer__tooltip-wrapper">
|
||||
<Button
|
||||
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--danger"
|
||||
onClick={(): void => setShowDeleteConfirm(true)}
|
||||
disabled={isRootUser || isSelf}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{isInvited ? 'Revoke Invite' : 'Delete Member'}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
<div className="edit-member-drawer__footer-divider" />
|
||||
<Button
|
||||
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--warning"
|
||||
onClick={handleGenerateResetLink}
|
||||
disabled={isGeneratingLink}
|
||||
>
|
||||
<RefreshCw size={12} />
|
||||
{isGeneratingLink
|
||||
? 'Generating...'
|
||||
: isInvited
|
||||
? 'Copy Invite Link'
|
||||
: 'Generate Password Reset Link'}
|
||||
</Button>
|
||||
<Tooltip title={isRootUser ? ROOT_USER_TOOLTIP : undefined}>
|
||||
<span className="edit-member-drawer__tooltip-wrapper">
|
||||
<Button
|
||||
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--warning"
|
||||
onClick={handleGenerateResetLink}
|
||||
disabled={isGeneratingLink || isRootUser}
|
||||
>
|
||||
<RefreshCw size={12} />
|
||||
{isGeneratingLink && 'Generating...'}
|
||||
{!isGeneratingLink && isInvited && 'Copy Invite Link'}
|
||||
{!isGeneratingLink && !isInvited && 'Generate Password Reset Link'}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__footer-right">
|
||||
@@ -318,7 +567,7 @@ function EditMemberDrawer({
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="sm"
|
||||
disabled={!isDirty || isSaving}
|
||||
disabled={!isDirty || isSaving || isRootUser}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Member Details'}
|
||||
@@ -328,22 +577,6 @@ function EditMemberDrawer({
|
||||
</div>
|
||||
);
|
||||
|
||||
const deleteDialogTitle = isInvited ? 'Revoke Invite' : 'Delete Member';
|
||||
const deleteDialogBody = isInvited ? (
|
||||
<>
|
||||
Are you sure you want to revoke the invite for{' '}
|
||||
<strong>{member?.email}</strong>? They will no longer be able to join the
|
||||
workspace using this invite.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Are you sure you want to delete{' '}
|
||||
<strong>{member?.name || member?.email}</strong>? This will remove their
|
||||
access to the workspace.
|
||||
</>
|
||||
);
|
||||
const deleteConfirmLabel = isInvited ? 'Revoke Invite' : 'Delete Member';
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerWrapper
|
||||
@@ -363,82 +596,25 @@ function EditMemberDrawer({
|
||||
className="edit-member-drawer"
|
||||
/>
|
||||
|
||||
<DialogWrapper
|
||||
<ResetLinkDialog
|
||||
open={showResetLinkDialog}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
setShowResetLinkDialog(false);
|
||||
setLinkType(null);
|
||||
}
|
||||
linkType={linkType}
|
||||
resetLink={resetLink}
|
||||
hasCopied={hasCopiedResetLink}
|
||||
onClose={(): void => {
|
||||
setShowResetLinkDialog(false);
|
||||
}}
|
||||
title={linkType === 'invite' ? 'Invite Link' : 'Password Reset Link'}
|
||||
showCloseButton
|
||||
width="base"
|
||||
className="reset-link-dialog"
|
||||
>
|
||||
<div className="reset-link-dialog__content">
|
||||
<p className="reset-link-dialog__description">
|
||||
{linkType === 'invite'
|
||||
? 'Share this one-time link with the team member to complete their account setup.'
|
||||
: 'This creates a one-time link the team member can use to set a new password for their SigNoz account.'}
|
||||
</p>
|
||||
<div className="reset-link-dialog__link-row">
|
||||
<div className="reset-link-dialog__link-text-wrap">
|
||||
<span className="reset-link-dialog__link-text">{resetLink}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={handleCopyResetLink}
|
||||
prefixIcon={
|
||||
hasCopiedResetLink ? <Check size={12} /> : <Copy size={12} />
|
||||
}
|
||||
className="reset-link-dialog__copy-btn"
|
||||
>
|
||||
{hasCopiedResetLink ? 'Copied!' : 'Copy'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogWrapper>
|
||||
onCopy={handleCopyResetLink}
|
||||
/>
|
||||
|
||||
<DialogWrapper
|
||||
<DeleteMemberDialog
|
||||
open={showDeleteConfirm}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
setShowDeleteConfirm(false);
|
||||
}
|
||||
}}
|
||||
title={deleteDialogTitle}
|
||||
width="narrow"
|
||||
className="alert-dialog delete-dialog"
|
||||
showCloseButton={false}
|
||||
disableOutsideClick={false}
|
||||
>
|
||||
<p className="delete-dialog__body">{deleteDialogBody}</p>
|
||||
|
||||
<DialogFooter className="delete-dialog__footer">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={(): void => setShowDeleteConfirm(false)}
|
||||
>
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
size="sm"
|
||||
disabled={isDeleting}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{isDeleting ? 'Processing...' : deleteConfirmLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogWrapper>
|
||||
isInvited={isInvited}
|
||||
member={member}
|
||||
isDeleting={isDeleting}
|
||||
onClose={(): void => setShowDeleteConfirm(false)}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
61
frontend/src/components/EditMemberDrawer/ResetLinkDialog.tsx
Normal file
61
frontend/src/components/EditMemberDrawer/ResetLinkDialog.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Button } from '@signozhq/button';
|
||||
import { DialogWrapper } from '@signozhq/dialog';
|
||||
import { Check, Copy } from '@signozhq/icons';
|
||||
|
||||
interface ResetLinkDialogProps {
|
||||
open: boolean;
|
||||
linkType: 'invite' | 'reset' | null;
|
||||
resetLink: string | null;
|
||||
hasCopied: boolean;
|
||||
onClose: () => void;
|
||||
onCopy: () => void;
|
||||
}
|
||||
|
||||
function ResetLinkDialog({
|
||||
open,
|
||||
linkType,
|
||||
resetLink,
|
||||
hasCopied,
|
||||
onClose,
|
||||
onCopy,
|
||||
}: ResetLinkDialogProps): JSX.Element {
|
||||
return (
|
||||
<DialogWrapper
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title={linkType === 'invite' ? 'Invite Link' : 'Password Reset Link'}
|
||||
showCloseButton
|
||||
width="base"
|
||||
className="reset-link-dialog"
|
||||
>
|
||||
<div className="reset-link-dialog__content">
|
||||
<p className="reset-link-dialog__description">
|
||||
{linkType === 'invite'
|
||||
? 'Share this one-time link with the team member to complete their account setup.'
|
||||
: 'This creates a one-time link the team member can use to set a new password for their SigNoz account.'}
|
||||
</p>
|
||||
<div className="reset-link-dialog__link-row">
|
||||
<div className="reset-link-dialog__link-text-wrap">
|
||||
<span className="reset-link-dialog__link-text">{resetLink}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={onCopy}
|
||||
prefixIcon={hasCopied ? <Check size={12} /> : <Copy size={12} />}
|
||||
className="reset-link-dialog__copy-btn"
|
||||
>
|
||||
{hasCopied ? 'Copied!' : 'Copy'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResetLinkDialog;
|
||||
@@ -4,11 +4,18 @@ import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
getResetPasswordToken,
|
||||
useDeleteUser,
|
||||
useUpdateUserDeprecated,
|
||||
useGetUser,
|
||||
useSetRoleByUserID,
|
||||
useUpdateMyUserV2,
|
||||
useUpdateUser,
|
||||
} from 'api/generated/services/users';
|
||||
import { MemberStatus } from 'container/MembersSettings/utils';
|
||||
import {
|
||||
listRolesSuccessResponse,
|
||||
managedRoles,
|
||||
} from 'mocks-server/__mockdata__/roles';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
import EditMemberDrawer, { EditMemberDrawerProps } from '../EditMemberDrawer';
|
||||
|
||||
@@ -44,7 +51,10 @@ jest.mock('@signozhq/dialog', () => ({
|
||||
|
||||
jest.mock('api/generated/services/users', () => ({
|
||||
useDeleteUser: jest.fn(),
|
||||
useUpdateUserDeprecated: jest.fn(),
|
||||
useGetUser: jest.fn(),
|
||||
useUpdateUser: jest.fn(),
|
||||
useUpdateMyUserV2: jest.fn(),
|
||||
useSetRoleByUserID: jest.fn(),
|
||||
getResetPasswordToken: jest.fn(),
|
||||
}));
|
||||
|
||||
@@ -69,25 +79,53 @@ jest.mock('react-use', () => ({
|
||||
],
|
||||
}));
|
||||
|
||||
const mockUpdateMutate = jest.fn();
|
||||
const ROLES_ENDPOINT = '*/api/v1/roles';
|
||||
|
||||
const mockDeleteMutate = jest.fn();
|
||||
const mockGetResetPasswordToken = jest.mocked(getResetPasswordToken);
|
||||
|
||||
const mockFetchedUser = {
|
||||
data: {
|
||||
id: 'user-1',
|
||||
displayName: 'Alice Smith',
|
||||
email: 'alice@signoz.io',
|
||||
status: 'active',
|
||||
userRoles: [
|
||||
{
|
||||
id: 'ur-1',
|
||||
roleId: managedRoles[0].id,
|
||||
role: managedRoles[0], // signoz-admin
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const activeMember = {
|
||||
id: 'user-1',
|
||||
name: 'Alice Smith',
|
||||
email: 'alice@signoz.io',
|
||||
role: 'ADMIN' as ROLES,
|
||||
status: MemberStatus.Active,
|
||||
joinedOn: '1700000000000',
|
||||
updatedAt: '1710000000000',
|
||||
};
|
||||
|
||||
const selfMember = {
|
||||
...activeMember,
|
||||
id: 'some-user-id',
|
||||
};
|
||||
|
||||
const rootMockFetchedUser = {
|
||||
data: {
|
||||
...mockFetchedUser.data,
|
||||
id: 'root-user-1',
|
||||
isRoot: true,
|
||||
},
|
||||
};
|
||||
|
||||
const invitedMember = {
|
||||
id: 'abc123',
|
||||
name: '',
|
||||
email: 'bob@signoz.io',
|
||||
role: 'VIEWER' as ROLES,
|
||||
status: MemberStatus.Invited,
|
||||
joinedOn: '1700000000000',
|
||||
};
|
||||
@@ -109,8 +147,26 @@ function renderDrawer(
|
||||
describe('EditMemberDrawer', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useUpdateUserDeprecated as jest.Mock).mockReturnValue({
|
||||
mutate: mockUpdateMutate,
|
||||
server.use(
|
||||
rest.get(ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
|
||||
),
|
||||
);
|
||||
(useGetUser as jest.Mock).mockReturnValue({
|
||||
data: mockFetchedUser,
|
||||
isLoading: false,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
(useUpdateUser as jest.Mock).mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
(useUpdateMyUserV2 as jest.Mock).mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
(useSetRoleByUserID as jest.Mock).mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
(useDeleteUser as jest.Mock).mockReturnValue({
|
||||
@@ -119,6 +175,10 @@ describe('EditMemberDrawer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('renders active member details and disables Save when form is not dirty', () => {
|
||||
renderDrawer();
|
||||
|
||||
@@ -130,16 +190,15 @@ describe('EditMemberDrawer', () => {
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('enables Save after editing name and calls update API on confirm', async () => {
|
||||
it('enables Save after editing name and calls updateUser on confirm', async () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockMutateAsync = jest.fn().mockResolvedValue({});
|
||||
|
||||
(useUpdateUserDeprecated as jest.Mock).mockImplementation((options) => ({
|
||||
mutate: mockUpdateMutate.mockImplementation(() => {
|
||||
options?.mutation?.onSuccess?.();
|
||||
}),
|
||||
(useUpdateUser as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isLoading: false,
|
||||
}));
|
||||
});
|
||||
|
||||
renderDrawer({ onComplete });
|
||||
|
||||
@@ -153,12 +212,90 @@ describe('EditMemberDrawer', () => {
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateMutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pathParams: { id: 'user-1' },
|
||||
data: expect.objectContaining({ displayName: 'Alice Updated' }),
|
||||
}),
|
||||
);
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith({
|
||||
pathParams: { id: 'user-1' },
|
||||
data: { displayName: 'Alice Updated' },
|
||||
});
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not close the drawer after a successful save', async () => {
|
||||
const onClose = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
renderDrawer({ onClose });
|
||||
|
||||
const nameInput = screen.getByDisplayValue('Alice Smith');
|
||||
await user.clear(nameInput);
|
||||
await user.type(nameInput, 'Alice Updated');
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /save member details/i });
|
||||
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save member details/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('selecting a different role calls setRole with the new role name', async () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockSet = jest.fn().mockResolvedValue({});
|
||||
|
||||
(useSetRoleByUserID as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockSet,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
renderDrawer({ onComplete });
|
||||
|
||||
// Open the roles dropdown and select signoz-editor
|
||||
await user.click(screen.getByLabelText('Roles'));
|
||||
await user.click(await screen.findByTitle('signoz-editor'));
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /save member details/i });
|
||||
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSet).toHaveBeenCalledWith({
|
||||
pathParams: { id: 'user-1' },
|
||||
data: { name: 'signoz-editor' },
|
||||
});
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not call removeRole when the role is changed', async () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockSet = jest.fn().mockResolvedValue({});
|
||||
|
||||
(useSetRoleByUserID as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockSet,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
renderDrawer({ onComplete });
|
||||
|
||||
// Switch from signoz-admin to signoz-viewer using single-select
|
||||
await user.click(screen.getByLabelText('Roles'));
|
||||
await user.click(await screen.findByTitle('signoz-viewer'));
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /save member details/i });
|
||||
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSet).toHaveBeenCalledWith({
|
||||
pathParams: { id: 'user-1' },
|
||||
data: { name: 'signoz-viewer' },
|
||||
});
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -239,16 +376,33 @@ describe('EditMemberDrawer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('calls update API when saving changes for an invited member', async () => {
|
||||
it('calls updateUser when saving name change for an invited member', async () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockMutateAsync = jest.fn().mockResolvedValue({});
|
||||
|
||||
(useUpdateUserDeprecated as jest.Mock).mockImplementation((options) => ({
|
||||
mutate: mockUpdateMutate.mockImplementation(() => {
|
||||
options?.mutation?.onSuccess?.();
|
||||
}),
|
||||
(useGetUser as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
data: {
|
||||
...mockFetchedUser.data,
|
||||
id: 'abc123',
|
||||
displayName: 'Bob',
|
||||
userRoles: [
|
||||
{
|
||||
id: 'ur-2',
|
||||
roleId: managedRoles[2].id,
|
||||
role: managedRoles[2], // signoz-viewer
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
}));
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
(useUpdateUser as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
renderDrawer({ member: { ...invitedMember, name: 'Bob' }, onComplete });
|
||||
|
||||
@@ -261,12 +415,10 @@ describe('EditMemberDrawer', () => {
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateMutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pathParams: { id: 'abc123' },
|
||||
data: expect.objectContaining({ displayName: 'Bob Updated' }),
|
||||
}),
|
||||
);
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith({
|
||||
pathParams: { id: 'abc123' },
|
||||
data: { displayName: 'Bob Updated' },
|
||||
});
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -280,16 +432,13 @@ describe('EditMemberDrawer', () => {
|
||||
} as ReturnType<typeof convertToApiError>);
|
||||
});
|
||||
|
||||
it('shows API error message when updateUser fails', async () => {
|
||||
it('shows SaveErrorItem when updateUser fails for name change', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockToast = jest.mocked(toast);
|
||||
|
||||
(useUpdateUserDeprecated as jest.Mock).mockImplementation((options) => ({
|
||||
mutate: mockUpdateMutate.mockImplementation(() => {
|
||||
options?.mutation?.onError?.({});
|
||||
}),
|
||||
(useUpdateUser as jest.Mock).mockReturnValue({
|
||||
mutateAsync: jest.fn().mockRejectedValue(new Error('server error')),
|
||||
isLoading: false,
|
||||
}));
|
||||
});
|
||||
|
||||
renderDrawer();
|
||||
|
||||
@@ -302,10 +451,9 @@ describe('EditMemberDrawer', () => {
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToast.error).toHaveBeenCalledWith(
|
||||
'Failed to update member details: Something went wrong on server',
|
||||
expect.anything(),
|
||||
);
|
||||
expect(
|
||||
screen.getByText('Name update: Something went wrong on server'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -364,6 +512,96 @@ describe('EditMemberDrawer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('self user (isSelf)', () => {
|
||||
it('disables Delete button when viewing own profile', () => {
|
||||
renderDrawer({ member: selfMember });
|
||||
expect(
|
||||
screen.getByRole('button', { name: /delete member/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('does not open delete confirm dialog when Delete is clicked while disabled (isSelf)', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderDrawer({ member: selfMember });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /delete member/i }));
|
||||
|
||||
expect(
|
||||
screen.queryByText(/are you sure you want to delete/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps name input enabled when viewing own profile', () => {
|
||||
renderDrawer({ member: selfMember });
|
||||
expect(screen.getByDisplayValue('Alice Smith')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('keeps Reset Link button enabled when viewing own profile', () => {
|
||||
renderDrawer({ member: selfMember });
|
||||
expect(
|
||||
screen.getByRole('button', { name: /generate password reset link/i }),
|
||||
).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('root user', () => {
|
||||
beforeEach(() => {
|
||||
(useGetUser as jest.Mock).mockReturnValue({
|
||||
data: rootMockFetchedUser,
|
||||
isLoading: false,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it('disables name input for root user', () => {
|
||||
renderDrawer();
|
||||
expect(screen.getByDisplayValue('Alice Smith')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables Delete button for root user', () => {
|
||||
renderDrawer();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /delete member/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables Reset Link button for root user', () => {
|
||||
renderDrawer();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /generate password reset link/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables Save button for root user', () => {
|
||||
renderDrawer();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save member details/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('does not open delete confirm dialog when Delete is clicked while disabled (root)', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderDrawer();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /delete member/i }));
|
||||
|
||||
expect(
|
||||
screen.queryByText(/are you sure you want to delete/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not call getResetPasswordToken when Reset Link is clicked while disabled (root)', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderDrawer();
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /generate password reset link/i }),
|
||||
);
|
||||
|
||||
expect(mockGetResetPasswordToken).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Generate Password Reset Link', () => {
|
||||
beforeEach(() => {
|
||||
mockCopyToClipboard.mockClear();
|
||||
|
||||
@@ -10,7 +10,12 @@ import APIError from 'types/api/error';
|
||||
import './ErrorContent.styles.scss';
|
||||
|
||||
interface ErrorContentProps {
|
||||
error: APIError;
|
||||
error:
|
||||
| APIError
|
||||
| {
|
||||
code: number;
|
||||
message: string;
|
||||
};
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
@@ -20,7 +25,15 @@ function ErrorContent({ error, icon }: ErrorContentProps): JSX.Element {
|
||||
errors: errorMessages,
|
||||
code: errorCode,
|
||||
message: errorMessage,
|
||||
} = error?.error?.error || {};
|
||||
} =
|
||||
error && 'error' in error
|
||||
? error?.error?.error || {}
|
||||
: {
|
||||
url: undefined,
|
||||
errors: [],
|
||||
code: error.code || 500,
|
||||
message: error.message || 'Something went wrong',
|
||||
};
|
||||
return (
|
||||
<section className="error-content">
|
||||
{/* Summary Header */}
|
||||
|
||||
@@ -197,13 +197,16 @@ function InviteMembersModal({
|
||||
})),
|
||||
});
|
||||
}
|
||||
toast.success('Invites sent successfully', { richColors: true });
|
||||
toast.success('Invites sent successfully', {
|
||||
richColors: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
resetAndClose();
|
||||
onComplete?.();
|
||||
} catch (err) {
|
||||
const apiErr = err as APIError;
|
||||
const errorMessage = apiErr?.getErrorMessage?.() ?? 'An error occurred';
|
||||
toast.error(errorMessage, { richColors: true });
|
||||
toast.error(errorMessage, { richColors: true, position: 'top-right' });
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
||||
@@ -28,14 +28,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
// In table/column view, keep action buttons visible at the viewport's right edge
|
||||
.log-line-action-buttons.table-view-log-actions {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 8px;
|
||||
left: auto;
|
||||
transform: translateY(-50%);
|
||||
margin: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.log-line-action-buttons {
|
||||
border: 1px solid var(--bg-vanilla-400);
|
||||
background: var(--bg-vanilla-400);
|
||||
|
||||
.ant-btn-default {
|
||||
}
|
||||
|
||||
.copy-log-btn {
|
||||
border-left: 1px solid var(--bg-vanilla-400);
|
||||
border-color: var(--bg-vanilla-400) !important;
|
||||
|
||||
@@ -15,13 +15,12 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
|
||||
letterSpacing: '-0.07px',
|
||||
marginBottom: '0px',
|
||||
minWidth: '10rem',
|
||||
width: '10rem',
|
||||
width: 'auto',
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultTableStyle: CSSProperties = {
|
||||
minWidth: '40rem',
|
||||
maxWidth: '90rem',
|
||||
};
|
||||
|
||||
export const defaultListViewPanelStyle: CSSProperties = {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ant-typography {
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
margin: 0;
|
||||
padding: 0px !important;
|
||||
&.small {
|
||||
font-size: 11px !important;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { TableColumnsType as ColumnsType, Typography } from 'antd';
|
||||
import { TableColumnsType as ColumnsType } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
|
||||
@@ -43,7 +43,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
const bodyColumnStyle = useMemo(
|
||||
() => ({
|
||||
...defaultTableStyle,
|
||||
...(fields.length > 2 ? { width: '50rem' } : {}),
|
||||
...(fields.length > 2 ? { width: 'auto' } : {}),
|
||||
}),
|
||||
[fields.length],
|
||||
);
|
||||
@@ -59,18 +59,18 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
key: name,
|
||||
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
props: {
|
||||
style: isListViewPanel
|
||||
? defaultListViewPanelStyle
|
||||
: getDefaultCellStyle(isDarkMode),
|
||||
style: {
|
||||
...(isListViewPanel
|
||||
? defaultListViewPanelStyle
|
||||
: getDefaultCellStyle(isDarkMode)),
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: linesPerRow,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
wordBreak: 'break-all',
|
||||
},
|
||||
},
|
||||
children: (
|
||||
<Typography.Paragraph
|
||||
ellipsis={{ rows: linesPerRow }}
|
||||
className={cx('paragraph', fontSize)}
|
||||
>
|
||||
{field}
|
||||
</Typography.Paragraph>
|
||||
),
|
||||
children: <p className={cx('paragraph', fontSize)}>{field}</p>,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -123,9 +123,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
return {
|
||||
children: (
|
||||
<div className="table-timestamp">
|
||||
<Typography.Paragraph ellipsis className={cx('text', fontSize)}>
|
||||
{date}
|
||||
</Typography.Paragraph>
|
||||
<p className={cx('text', fontSize)}>{date}</p>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -4,9 +4,7 @@ import { Table, Tooltip } from 'antd';
|
||||
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { MemberStatus } from 'container/MembersSettings/utils';
|
||||
import { capitalize } from 'lodash-es';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
import './MembersTable.styles.scss';
|
||||
|
||||
@@ -14,7 +12,6 @@ export interface MemberRow {
|
||||
id: string;
|
||||
name?: string;
|
||||
email: string;
|
||||
role: ROLES;
|
||||
status: MemberStatus;
|
||||
joinedOn: string | null;
|
||||
updatedAt?: string | null;
|
||||
@@ -141,17 +138,6 @@ function MembersTable({
|
||||
<NameEmailCell name={record.name} email={record.email} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Roles',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
width: 180,
|
||||
sorter: (a, b): number => a.role.localeCompare(b.role),
|
||||
render: (role: ROLES): JSX.Element => (
|
||||
<Badge color="vanilla">{capitalize(role)}</Badge>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { MemberStatus } from 'container/MembersSettings/utils';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
import MembersTable, { MemberRow } from '../MembersTable';
|
||||
|
||||
@@ -9,7 +8,6 @@ const mockActiveMembers: MemberRow[] = [
|
||||
id: 'user-1',
|
||||
name: 'Alice Smith',
|
||||
email: 'alice@signoz.io',
|
||||
role: 'ADMIN' as ROLES,
|
||||
status: MemberStatus.Active,
|
||||
joinedOn: '1700000000000',
|
||||
},
|
||||
@@ -17,7 +15,6 @@ const mockActiveMembers: MemberRow[] = [
|
||||
id: 'user-2',
|
||||
name: 'Bob Jones',
|
||||
email: 'bob@signoz.io',
|
||||
role: 'VIEWER' as ROLES,
|
||||
status: MemberStatus.Active,
|
||||
joinedOn: null,
|
||||
},
|
||||
@@ -27,7 +24,6 @@ const mockInvitedMember: MemberRow = {
|
||||
id: 'inv-abc',
|
||||
name: '',
|
||||
email: 'charlie@signoz.io',
|
||||
role: 'EDITOR' as ROLES,
|
||||
status: MemberStatus.Invited,
|
||||
joinedOn: null,
|
||||
};
|
||||
@@ -47,12 +43,11 @@ describe('MembersTable', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders member rows with name, email, role badge, and ACTIVE status', () => {
|
||||
it('renders member rows with name, email, and ACTIVE status', () => {
|
||||
render(<MembersTable {...defaultProps} data={mockActiveMembers} />);
|
||||
|
||||
expect(screen.getByText('Alice Smith')).toBeInTheDocument();
|
||||
expect(screen.getByText('alice@signoz.io')).toBeInTheDocument();
|
||||
expect(screen.getByText('Admin')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('ACTIVE')).toHaveLength(2);
|
||||
});
|
||||
|
||||
@@ -67,7 +62,6 @@ describe('MembersTable', () => {
|
||||
|
||||
expect(screen.getByText('INVITED')).toBeInTheDocument();
|
||||
expect(screen.getByText('charlie@signoz.io')).toBeInTheDocument();
|
||||
expect(screen.getByText('Editor')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onRowClick with the member data when a row is clicked', async () => {
|
||||
@@ -99,7 +93,6 @@ describe('MembersTable', () => {
|
||||
id: 'user-del',
|
||||
name: 'Dave Deleted',
|
||||
email: 'dave@signoz.io',
|
||||
role: 'VIEWER' as ROLES,
|
||||
status: MemberStatus.Deleted,
|
||||
joinedOn: null,
|
||||
};
|
||||
|
||||
@@ -1,21 +1,35 @@
|
||||
.quick-filters-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
|
||||
.quick-filters-settings-container {
|
||||
flex: 0 0 0;
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
border-right: 1px solid var(--bg-slate-400);
|
||||
color: var(--bg-vanilla-100);
|
||||
|
||||
.overlay-scrollbar {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
width: 342px;
|
||||
background: var(--bg-slate-500);
|
||||
|
||||
@@ -88,3 +88,13 @@
|
||||
color: var(--destructive);
|
||||
}
|
||||
}
|
||||
|
||||
.roles-single-select {
|
||||
.ant-select-selector {
|
||||
min-height: 32px;
|
||||
background-color: var(--l2-background) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 2px;
|
||||
padding: 2px var(--padding-2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,10 +158,10 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
|
||||
return (
|
||||
<Select
|
||||
id={id}
|
||||
value={value}
|
||||
value={value || undefined}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
className={cx('roles-select', className)}
|
||||
className={cx('roles-single-select', className)}
|
||||
loading={loading}
|
||||
notFoundContent={notFoundContent}
|
||||
options={options}
|
||||
|
||||
@@ -16,8 +16,8 @@ interface OverviewTabProps {
|
||||
account: ServiceAccountRow;
|
||||
localName: string;
|
||||
onNameChange: (v: string) => void;
|
||||
localRoles: string[];
|
||||
onRolesChange: (v: string[]) => void;
|
||||
localRole: string;
|
||||
onRoleChange: (v: string) => void;
|
||||
isDisabled: boolean;
|
||||
availableRoles: AuthtypesRoleDTO[];
|
||||
rolesLoading?: boolean;
|
||||
@@ -31,8 +31,8 @@ function OverviewTab({
|
||||
account,
|
||||
localName,
|
||||
onNameChange,
|
||||
localRoles,
|
||||
onRolesChange,
|
||||
localRole,
|
||||
onRoleChange,
|
||||
isDisabled,
|
||||
availableRoles,
|
||||
rolesLoading,
|
||||
@@ -96,15 +96,10 @@ function OverviewTab({
|
||||
{isDisabled ? (
|
||||
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
|
||||
<div className="sa-drawer__disabled-roles">
|
||||
{localRoles.length > 0 ? (
|
||||
localRoles.map((roleId) => {
|
||||
const role = availableRoles.find((r) => r.id === roleId);
|
||||
return (
|
||||
<Badge key={roleId} color="vanilla">
|
||||
{role?.name ?? roleId}
|
||||
</Badge>
|
||||
);
|
||||
})
|
||||
{localRole ? (
|
||||
<Badge color="vanilla">
|
||||
{availableRoles.find((r) => r.id === localRole)?.name ?? localRole}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="sa-drawer__input-text">—</span>
|
||||
)}
|
||||
@@ -114,15 +109,14 @@ function OverviewTab({
|
||||
) : (
|
||||
<RolesSelect
|
||||
id="sa-roles"
|
||||
mode="multiple"
|
||||
roles={availableRoles}
|
||||
loading={rolesLoading}
|
||||
isError={rolesError}
|
||||
error={rolesErrorObj}
|
||||
onRefetch={onRefetchRoles}
|
||||
value={localRoles}
|
||||
onChange={onRolesChange}
|
||||
placeholder="Select roles"
|
||||
value={localRole}
|
||||
onChange={onRoleChange}
|
||||
placeholder="Select role"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -80,7 +80,7 @@ function ServiceAccountDrawer({
|
||||
parseAsBoolean.withDefault(false),
|
||||
);
|
||||
const [localName, setLocalName] = useState('');
|
||||
const [localRoles, setLocalRoles] = useState<string[]>([]);
|
||||
const [localRole, setLocalRole] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveErrors, setSaveErrors] = useState<SaveError[]>([]);
|
||||
|
||||
@@ -116,7 +116,7 @@ function ServiceAccountDrawer({
|
||||
}, [account?.id, account?.name, setKeysPage]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalRoles(currentRoles.map((r) => r.id).filter(Boolean) as string[]);
|
||||
setLocalRole(currentRoles[0]?.id ?? '');
|
||||
}, [currentRoles]);
|
||||
|
||||
const isDeleted =
|
||||
@@ -125,8 +125,7 @@ function ServiceAccountDrawer({
|
||||
const isDirty =
|
||||
account !== null &&
|
||||
(localName !== (account.name ?? '') ||
|
||||
JSON.stringify([...localRoles].sort()) !==
|
||||
JSON.stringify([...currentRoles.map((r) => r.id).filter(Boolean)].sort()));
|
||||
localRole !== (currentRoles[0]?.id ?? ''));
|
||||
|
||||
const {
|
||||
roles: availableRoles,
|
||||
@@ -216,7 +215,10 @@ function ServiceAccountDrawer({
|
||||
|
||||
const retryRolesUpdate = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const failures = await applyDiff(localRoles, availableRoles);
|
||||
const failures = await applyDiff(
|
||||
[localRole].filter(Boolean),
|
||||
availableRoles,
|
||||
);
|
||||
if (failures.length === 0) {
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Roles update'));
|
||||
} else {
|
||||
@@ -240,7 +242,7 @@ function ServiceAccountDrawer({
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [localRoles, availableRoles, applyDiff, toSaveApiError, makeRoleRetry]);
|
||||
}, [localRole, availableRoles, applyDiff, toSaveApiError, makeRoleRetry]);
|
||||
|
||||
const handleSave = useCallback(async (): Promise<void> => {
|
||||
if (!account || !isDirty) {
|
||||
@@ -259,7 +261,7 @@ function ServiceAccountDrawer({
|
||||
|
||||
const [nameResult, rolesResult] = await Promise.allSettled([
|
||||
namePromise,
|
||||
applyDiff(localRoles, availableRoles),
|
||||
applyDiff([localRole].filter(Boolean), availableRoles),
|
||||
]);
|
||||
|
||||
const errors: SaveError[] = [];
|
||||
@@ -294,6 +296,7 @@ function ServiceAccountDrawer({
|
||||
} else {
|
||||
toast.success('Service account updated successfully', {
|
||||
richColors: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
onSuccess({ closeDrawer: false });
|
||||
}
|
||||
@@ -307,7 +310,7 @@ function ServiceAccountDrawer({
|
||||
account,
|
||||
isDirty,
|
||||
localName,
|
||||
localRoles,
|
||||
localRole,
|
||||
availableRoles,
|
||||
updateMutateAsync,
|
||||
applyDiff,
|
||||
@@ -409,8 +412,8 @@ function ServiceAccountDrawer({
|
||||
account={account}
|
||||
localName={localName}
|
||||
onNameChange={handleNameChange}
|
||||
localRoles={localRoles}
|
||||
onRolesChange={setLocalRoles}
|
||||
localRole={localRole}
|
||||
onRoleChange={setLocalRole}
|
||||
isDisabled={isDeleted}
|
||||
availableRoles={availableRoles}
|
||||
rolesLoading={rolesLoading}
|
||||
|
||||
@@ -139,20 +139,20 @@ describe('ServiceAccountDrawer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('changing roles enables Save; clicking Save sends updated roles in payload', async () => {
|
||||
const updateSpy = jest.fn();
|
||||
it('changing roles enables Save; clicking Save sends role add request without delete', async () => {
|
||||
const roleSpy = jest.fn();
|
||||
const deleteSpy = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.put(SA_ENDPOINT, async (req, res, ctx) => {
|
||||
updateSpy(await req.json());
|
||||
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
|
||||
}),
|
||||
rest.post(SA_ROLES_ENDPOINT, async (req, res, ctx) => {
|
||||
roleSpy(await req.json());
|
||||
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
|
||||
}),
|
||||
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) => {
|
||||
deleteSpy();
|
||||
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
|
||||
}),
|
||||
);
|
||||
|
||||
renderDrawer();
|
||||
@@ -167,12 +167,12 @@ describe('ServiceAccountDrawer', () => {
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateSpy).not.toHaveBeenCalled();
|
||||
expect(roleSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: '019c24aa-2248-7585-a129-4188b3473c27',
|
||||
}),
|
||||
);
|
||||
expect(deleteSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -350,7 +350,7 @@ describe('ServiceAccountDrawer – save-error UX', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('role update failure shows SaveErrorItem with the role name context', async () => {
|
||||
it('role add failure shows SaveErrorItem with the role name context', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
|
||||
@@ -12,6 +12,7 @@ export enum LOCALSTORAGE {
|
||||
GRAPH_VISIBILITY_STATES = 'GRAPH_VISIBILITY_STATES',
|
||||
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMN_SIZING = 'LOGS_LIST_COLUMN_SIZING',
|
||||
LOGGED_IN_USER_NAME = 'LOGGED_IN_USER_NAME',
|
||||
LOGGED_IN_USER_EMAIL = 'LOGGED_IN_USER_EMAIL',
|
||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||
|
||||
@@ -13,7 +13,7 @@ import uPlot from 'uplot';
|
||||
|
||||
import { ChartProps } from '../types';
|
||||
|
||||
const TOOLTIP_WIDTH_PADDING = 60;
|
||||
const TOOLTIP_WIDTH_PADDING = 120;
|
||||
const TOOLTIP_MIN_WIDTH = 200;
|
||||
|
||||
export default function ChartWrapper({
|
||||
|
||||
@@ -838,22 +838,20 @@ function FormAlertRules({
|
||||
>
|
||||
<div className="overview-header">
|
||||
<div className="alert-type-container">
|
||||
{isNewRule && (
|
||||
<Typography.Title level={5} className="alert-type-title">
|
||||
<BellDot size={14} />
|
||||
<Typography.Title level={5} className="alert-type-title">
|
||||
<BellDot size={14} />
|
||||
|
||||
{alertDef.alertType === AlertTypes.ANOMALY_BASED_ALERT &&
|
||||
'Anomaly Detection Alert'}
|
||||
{alertDef.alertType === AlertTypes.METRICS_BASED_ALERT &&
|
||||
'Metrics Based Alert'}
|
||||
{alertDef.alertType === AlertTypes.LOGS_BASED_ALERT &&
|
||||
'Logs Based Alert'}
|
||||
{alertDef.alertType === AlertTypes.TRACES_BASED_ALERT &&
|
||||
'Traces Based Alert'}
|
||||
{alertDef.alertType === AlertTypes.EXCEPTIONS_BASED_ALERT &&
|
||||
'Exceptions Based Alert'}
|
||||
</Typography.Title>
|
||||
)}
|
||||
{alertDef.alertType === AlertTypes.ANOMALY_BASED_ALERT &&
|
||||
'Anomaly Detection Alert'}
|
||||
{alertDef.alertType === AlertTypes.METRICS_BASED_ALERT &&
|
||||
'Metrics Based Alert'}
|
||||
{alertDef.alertType === AlertTypes.LOGS_BASED_ALERT &&
|
||||
'Logs Based Alert'}
|
||||
{alertDef.alertType === AlertTypes.TRACES_BASED_ALERT &&
|
||||
'Traces Based Alert'}
|
||||
{alertDef.alertType === AlertTypes.EXCEPTIONS_BASED_ALERT &&
|
||||
'Exceptions Based Alert'}
|
||||
</Typography.Title>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -1,41 +1,52 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useQuery } from 'react-query';
|
||||
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
|
||||
import {
|
||||
getHostLists,
|
||||
HostListPayload,
|
||||
HostListResponse,
|
||||
} from 'api/infraMonitoring/getHostLists';
|
||||
import HostMetricDetail from 'components/HostMetricsDetail';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringFiltersHosts,
|
||||
useInfraMonitoringOrderByHosts,
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
|
||||
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Filter } from 'lucide-react';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import {
|
||||
getAutoRefreshQueryKey,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime/utils';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { FeatureKeys } from '../../constants/features';
|
||||
import { useAppContext } from '../../providers/App/App';
|
||||
import HostsListControls from './HostsListControls';
|
||||
import HostsListTable from './HostsListTable';
|
||||
import { getHostListsQuery, GetHostsQuickFiltersConfig } from './utils';
|
||||
|
||||
import './InfraMonitoring.styles.scss';
|
||||
function HostsList(): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const defaultFilters: TagFilter = { items: [], op: 'and' };
|
||||
const baseQuery = getHostListsQuery();
|
||||
|
||||
function HostsList(): JSX.Element {
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [filters, setFilters] = useInfraMonitoringFiltersHosts();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderByHosts();
|
||||
@@ -62,57 +73,49 @@ function HostsList(): JSX.Element {
|
||||
|
||||
const { pageSize, setPageSize } = usePageSize('hosts');
|
||||
|
||||
const query = useMemo(() => {
|
||||
const baseQuery = getHostListsQuery();
|
||||
return {
|
||||
...baseQuery,
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy,
|
||||
};
|
||||
}, [pageSize, currentPage, filters, minTime, maxTime, orderBy]);
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
|
||||
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedHostName) {
|
||||
return [
|
||||
'hostList',
|
||||
const queryKey = useMemo(
|
||||
() =>
|
||||
getAutoRefreshQueryKey(
|
||||
selectedTime,
|
||||
REACT_QUERY_KEY.GET_HOST_LIST,
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(filters),
|
||||
JSON.stringify(orderBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'hostList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(filters),
|
||||
JSON.stringify(orderBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
pageSize,
|
||||
currentPage,
|
||||
filters,
|
||||
orderBy,
|
||||
selectedHostName,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetHostList(
|
||||
query as HostListPayload,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
),
|
||||
[pageSize, currentPage, filters, orderBy, selectedTime],
|
||||
);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useQuery<
|
||||
SuccessResponse<HostListResponse> | ErrorResponse,
|
||||
Error
|
||||
>({
|
||||
queryKey,
|
||||
queryFn: ({ signal }) => {
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
const payload: HostListPayload = {
|
||||
...baseQuery,
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters: filters ?? defaultFilters,
|
||||
orderBy,
|
||||
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
|
||||
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
|
||||
};
|
||||
|
||||
return getHostLists(payload, signal);
|
||||
},
|
||||
enabled: true,
|
||||
keepPreviousData: true,
|
||||
refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
});
|
||||
|
||||
const hostMetricsData = useMemo(() => data?.payload?.data?.records || [], [
|
||||
data,
|
||||
]);
|
||||
@@ -227,7 +230,7 @@ function HostsList(): JSX.Element {
|
||||
isError={isError}
|
||||
tableData={data}
|
||||
hostMetricsData={hostMetricsData}
|
||||
filters={filters || { items: [], op: 'AND' }}
|
||||
filters={filters ?? defaultFilters}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
onHostClick={handleHostClick}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from 'antd';
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
@@ -26,9 +27,40 @@ import {
|
||||
function EmptyOrLoadingView(
|
||||
viewState: EmptyOrLoadingViewProps,
|
||||
): React.ReactNode {
|
||||
const { isError, errorMessage } = viewState;
|
||||
if (isError) {
|
||||
return <Typography>{errorMessage || 'Something went wrong'}</Typography>;
|
||||
if (viewState.showTableLoadingState) {
|
||||
return (
|
||||
<div className="hosts-list-loading-state">
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { isError, data } = viewState;
|
||||
if (isError || data?.error || (data?.statusCode || 0) >= 300) {
|
||||
return (
|
||||
<ErrorContent
|
||||
error={{
|
||||
code: data?.statusCode || 500,
|
||||
message: data?.error || 'Something went wrong',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (viewState.showHostsEmptyState) {
|
||||
return (
|
||||
@@ -76,30 +108,6 @@ function EmptyOrLoadingView(
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (viewState.showTableLoadingState) {
|
||||
return (
|
||||
<div className="hosts-list-loading-state">
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -190,7 +198,8 @@ export default function HostsListTable({
|
||||
!isLoading &&
|
||||
formattedHostMetricsData.length === 0 &&
|
||||
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
|
||||
!filters.items.length;
|
||||
!filters.items.length &&
|
||||
!endTimeBeforeRetention;
|
||||
|
||||
const showEndTimeBeforeRetentionMessage =
|
||||
!isFetching &&
|
||||
@@ -211,7 +220,7 @@ export default function HostsListTable({
|
||||
|
||||
const emptyOrLoadingView = EmptyOrLoadingView({
|
||||
isError,
|
||||
errorMessage: data?.error ?? '',
|
||||
data,
|
||||
showHostsEmptyState,
|
||||
sentAnyHostMetricsData,
|
||||
isSendingIncorrectK8SAgentMetrics,
|
||||
|
||||
@@ -2,8 +2,8 @@ import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render } from '@testing-library/react';
|
||||
import * as useGetHostListHooks from 'hooks/infraMonitoring/useGetHostList';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import * as getHostListsApi from 'api/infraMonitoring/getHostLists';
|
||||
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import * as timezoneHooks from 'providers/Timezone';
|
||||
@@ -19,6 +19,10 @@ jest.mock('lib/getMinMax', () => ({
|
||||
maxTime: 1713738000000,
|
||||
isValidShortHandDateTimeFormat: jest.fn().mockReturnValue(true),
|
||||
})),
|
||||
getMinMaxForSelectedTime: jest.fn().mockReturnValue({
|
||||
minTime: 1713734400000000000,
|
||||
maxTime: 1713738000000000000,
|
||||
}),
|
||||
}));
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
||||
__esModule: true,
|
||||
@@ -41,7 +45,13 @@ jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
@@ -80,27 +90,40 @@ jest.spyOn(timezoneHooks, 'useTimezone').mockReturnValue({
|
||||
offset: 0,
|
||||
},
|
||||
} as any);
|
||||
jest.spyOn(useGetHostListHooks, 'useGetHostList').mockReturnValue({
|
||||
data: {
|
||||
payload: {
|
||||
data: {
|
||||
records: [
|
||||
{
|
||||
hostName: 'test-host',
|
||||
active: true,
|
||||
cpu: 0.75,
|
||||
memory: 0.65,
|
||||
wait: 0.03,
|
||||
},
|
||||
],
|
||||
isSendingK8SAgentMetrics: false,
|
||||
sentAnyHostMetricsData: true,
|
||||
},
|
||||
|
||||
jest.spyOn(getHostListsApi, 'getHostLists').mockResolvedValue({
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: {
|
||||
status: 'success',
|
||||
data: {
|
||||
type: 'list',
|
||||
records: [
|
||||
{
|
||||
hostName: 'test-host',
|
||||
active: true,
|
||||
os: 'linux',
|
||||
cpu: 0.75,
|
||||
cpuTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
memory: 0.65,
|
||||
memoryTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
wait: 0.03,
|
||||
waitTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
load15: 0.5,
|
||||
load15TimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
},
|
||||
],
|
||||
groups: null,
|
||||
total: 1,
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
endTimeBeforeRetention: false,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
} as any);
|
||||
params: {} as any,
|
||||
});
|
||||
|
||||
jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
|
||||
user: {
|
||||
role: 'admin',
|
||||
@@ -128,22 +151,11 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
|
||||
const Wrapper = withNuqsTestingAdapter({ searchParams: {} });
|
||||
|
||||
describe('HostsList', () => {
|
||||
it('renders hosts list table', () => {
|
||||
const { container } = render(
|
||||
<Wrapper>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<HostsList />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-table')).toBeInTheDocument();
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
it('renders filters', () => {
|
||||
it('renders hosts list table', async () => {
|
||||
const { container } = render(
|
||||
<Wrapper>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -155,6 +167,25 @@ describe('HostsList', () => {
|
||||
</QueryClientProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(container.querySelector('.filters')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.hosts-list-table')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders filters', async () => {
|
||||
const { container } = render(
|
||||
<Wrapper>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<HostsList />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.filters')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, TabsProps, Tag, Tooltip, Typography } from 'antd';
|
||||
import { TableColumnType as ColumnType } from 'antd';
|
||||
import {
|
||||
Progress,
|
||||
TableColumnType as ColumnType,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { SortOrder } from 'antd/lib/table/interface';
|
||||
import {
|
||||
HostData,
|
||||
@@ -13,8 +18,6 @@ import {
|
||||
FiltersType,
|
||||
IQuickFiltersConfig,
|
||||
} from 'components/QuickFilters/types';
|
||||
import TabLabel from 'components/TabLabel';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
@@ -22,9 +25,6 @@ import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { OrderBySchemaType } from '../InfraMonitoringK8s/schemas';
|
||||
import HostsList from './HostsList';
|
||||
|
||||
import './InfraMonitoring.styles.scss';
|
||||
|
||||
export interface HostRowData {
|
||||
key?: string;
|
||||
@@ -112,7 +112,10 @@ export interface HostsListTableProps {
|
||||
|
||||
export interface EmptyOrLoadingViewProps {
|
||||
isError: boolean;
|
||||
errorMessage: string;
|
||||
data:
|
||||
| ErrorResponse<string>
|
||||
| SuccessResponse<HostListResponse, unknown>
|
||||
| undefined;
|
||||
showHostsEmptyState: boolean;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingIncorrectK8SAgentMetrics: boolean;
|
||||
@@ -141,14 +144,6 @@ function mapOrderByToSortOrder(
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export const getTabsItems = (): TabsProps['items'] => [
|
||||
{
|
||||
label: <TabLabel label="List View" isDisabled={false} tooltipText="" />,
|
||||
key: PANEL_TYPES.LIST,
|
||||
children: <HostsList />,
|
||||
},
|
||||
];
|
||||
|
||||
export const getHostsListColumns = (
|
||||
orderBy: OrderBySchemaType,
|
||||
): ColumnType<HostRowData>[] => [
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
.clickableRow {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expandedClickableRow {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expandedTableContainer {
|
||||
border: 1px solid var(--l1-border);
|
||||
overflow-x: auto;
|
||||
padding-left: 48px;
|
||||
|
||||
:global(.ant-table-tbody > tr:hover > td) {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.expandedTableFooter {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
padding-left: 42px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.viewAllButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.k8sListTable {
|
||||
:global(.ant-table) {
|
||||
:global(.ant-table-thead > tr > th) {
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
border-bottom: none;
|
||||
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
background: var(--bg-ink-500) !important;
|
||||
|
||||
&::before {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-table-cell) {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--l1-foreground);
|
||||
background: var(--bg-ink-500);
|
||||
border-bottom: none;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.hostname-column-value) {
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
:global(.progress-container) {
|
||||
:global(.ant-progress-bg) {
|
||||
height: 8px !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-table-tbody > tr:hover > td) {
|
||||
background: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
:global(.ant-table-tbody > tr:not(.ant-table-expanded-row):hover > td) {
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
:global(.ant-table-tbody > tr > td) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:global(.ant-empty-normal) {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-pagination) {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
background-color: var(--bg-ink-500);
|
||||
margin: 0 !important;
|
||||
padding-right: 72px;
|
||||
|
||||
:global(.ant-pagination-item) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item-active) {
|
||||
background: var(--l2-background);
|
||||
border-color: var(--l2-border);
|
||||
|
||||
a {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.noFilteredHostsMessageContainer {
|
||||
height: 30vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.noFilteredHostsMessageContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
width: fit-content;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.noFilteredHostsMessage {
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.emptyStateSvg {
|
||||
width: 32px;
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -1,615 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Spin,
|
||||
Table,
|
||||
TableColumnType as ColumnType,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { ChevronDown, ChevronRight, CornerDownRight } from 'lucide-react';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import {
|
||||
getAutoRefreshQueryKey,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime/utils';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { K8sCategory } from '../constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringFilters,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringQueryFilters,
|
||||
useInfraMonitoringSelectedItem,
|
||||
} from '../hooks';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
import { OrderBySchemaType } from '../schemas';
|
||||
import { usePageSize } from '../utils';
|
||||
import K8sHeader from './K8sHeader';
|
||||
import {
|
||||
IEntityColumn,
|
||||
useInfraMonitoringTableColumnsForPage,
|
||||
useInfraMonitoringTableColumnsStore,
|
||||
} from './useInfraMonitoringTableColumnsStore';
|
||||
|
||||
import styles from './K8sBaseList.module.scss';
|
||||
|
||||
export type K8sBaseFilters = {
|
||||
filters?: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
start: number;
|
||||
end: number;
|
||||
orderBy?: OrderBySchemaType;
|
||||
};
|
||||
|
||||
export type K8sRenderedRowData = {
|
||||
/**
|
||||
* The unique ID for the row
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* The ID to the selectedItem
|
||||
*/
|
||||
itemKey: string;
|
||||
groupedByMeta: Record<string, string>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type K8sBaseListProps<T = unknown> = {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
entity: K8sCategory;
|
||||
tableColumnsDefinitions: IEntityColumn[];
|
||||
tableColumns: ColumnType<K8sRenderedRowData>[];
|
||||
fetchListData: (
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{
|
||||
data: T[];
|
||||
total: number;
|
||||
error?: string | null;
|
||||
}>;
|
||||
renderRowData: (
|
||||
record: T,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
) => K8sRenderedRowData;
|
||||
eventCategory: InfraMonitoringEvents;
|
||||
};
|
||||
|
||||
export type K8sExpandedRowProps<T> = {
|
||||
record: K8sRenderedRowData;
|
||||
entity: K8sCategory;
|
||||
tableColumns: ColumnType<K8sRenderedRowData>[];
|
||||
fetchListData: K8sBaseListProps<T>['fetchListData'];
|
||||
renderRowData: K8sBaseListProps<T>['renderRowData'];
|
||||
};
|
||||
|
||||
function K8sExpandedRow<T>({
|
||||
record,
|
||||
entity,
|
||||
tableColumns,
|
||||
fetchListData,
|
||||
renderRowData,
|
||||
}: K8sExpandedRowProps<T>): JSX.Element {
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
const [, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [, setFilters] = useInfraMonitoringFilters();
|
||||
const [, setSelectedItem] = useInfraMonitoringSelectedItem();
|
||||
|
||||
const queryFilters = useInfraMonitoringQueryFilters();
|
||||
|
||||
const [
|
||||
columnsDefinitions,
|
||||
columnsHidden,
|
||||
] = useInfraMonitoringTableColumnsForPage(entity);
|
||||
|
||||
const hiddenColumnIdsForNested = useMemo(
|
||||
() =>
|
||||
columnsDefinitions
|
||||
.filter((col) => col.behavior === 'hidden-on-collapse')
|
||||
.map((col) => col.id),
|
||||
[columnsDefinitions],
|
||||
);
|
||||
|
||||
const nestedColumns = useMemo(
|
||||
() =>
|
||||
tableColumns.filter(
|
||||
(c) =>
|
||||
!columnsHidden.includes(c.key?.toString() || '') &&
|
||||
!hiddenColumnIdsForNested.includes(c.key?.toString() || ''),
|
||||
),
|
||||
[tableColumns, columnsHidden, hiddenColumnIdsForNested],
|
||||
);
|
||||
|
||||
const createFiltersForRecord = useCallback((): IBuilderQuery['filters'] => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...queryFilters.items],
|
||||
op: 'and',
|
||||
};
|
||||
|
||||
const { groupedByMeta } = record;
|
||||
|
||||
for (const key of Object.keys(groupedByMeta)) {
|
||||
baseFilters.items.push({
|
||||
key: {
|
||||
key,
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: groupedByMeta[key],
|
||||
id: key,
|
||||
});
|
||||
}
|
||||
|
||||
return baseFilters;
|
||||
}, [queryFilters.items, record]);
|
||||
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
|
||||
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
return getAutoRefreshQueryKey(selectedTime, [
|
||||
'k8sExpandedRow',
|
||||
record.key,
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
]);
|
||||
}, [selectedTime, record.key, queryFilters, orderBy]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useQuery({
|
||||
queryKey,
|
||||
queryFn: ({ signal }) => {
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
return fetchListData(
|
||||
{
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
filters: createFiltersForRecord(),
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || undefined,
|
||||
groupBy: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
},
|
||||
refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
});
|
||||
|
||||
const formattedData = useMemo(
|
||||
() => data?.data?.map((item) => renderRowData(item, groupBy)),
|
||||
[data?.data, renderRowData, groupBy],
|
||||
);
|
||||
|
||||
const openRecordInNewTab = (rowRecord: K8sRenderedRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set('selectedItem', rowRecord.itemKey);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleViewAllClick = (): void => {
|
||||
const filters = createFiltersForRecord();
|
||||
setFilters(JSON.stringify(filters));
|
||||
setCurrentPage(1);
|
||||
setGroupBy([]);
|
||||
setOrderBy(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.expandedTableContainer}
|
||||
data-testid="expanded-table-container"
|
||||
>
|
||||
{isError && (
|
||||
<Typography>{data?.error?.toString() || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
|
||||
{isFetching || isLoading ? (
|
||||
<LoadingContainer />
|
||||
) : (
|
||||
<div data-testid="expanded-table">
|
||||
<Table
|
||||
columns={nestedColumns}
|
||||
dataSource={formattedData}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
showHeader={false}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
onRow={(
|
||||
rowRecord: K8sRenderedRowData,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
openRecordInNewTab(rowRecord);
|
||||
return;
|
||||
}
|
||||
setSelectedItem(rowRecord.itemKey);
|
||||
},
|
||||
className: styles.expandedClickableRow,
|
||||
})}
|
||||
/>
|
||||
|
||||
{data?.total && data?.total > 10 && (
|
||||
<div className={styles.expandedTableFooter}>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
className={styles.viewAllButton}
|
||||
onClick={handleViewAllClick}
|
||||
>
|
||||
<CornerDownRight size={14} />
|
||||
View All
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function K8sBaseList<T>({
|
||||
controlListPrefix,
|
||||
entity,
|
||||
tableColumnsDefinitions,
|
||||
tableColumns,
|
||||
fetchListData,
|
||||
renderRowData,
|
||||
eventCategory,
|
||||
}: K8sBaseListProps<T>): JSX.Element {
|
||||
const queryFilters = useInfraMonitoringQueryFilters();
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [groupBy] = useInfraMonitoringGroupBy();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
const [initialOrderBy] = useState(orderBy);
|
||||
const [selectedItem, setSelectedItem] = useQueryState(
|
||||
'selectedItem',
|
||||
parseAsString,
|
||||
);
|
||||
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
setExpandedRowKeys([]);
|
||||
}, [groupBy, currentPage]);
|
||||
const { pageSize, setPageSize } = usePageSize(entity);
|
||||
|
||||
const initializeTableColumns = useInfraMonitoringTableColumnsStore(
|
||||
(state) => state.initializePageColumns,
|
||||
);
|
||||
useEffect(() => {
|
||||
initializeTableColumns(entity, tableColumnsDefinitions);
|
||||
}, [initializeTableColumns, entity, tableColumnsDefinitions]);
|
||||
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
|
||||
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
return getAutoRefreshQueryKey(
|
||||
selectedTime,
|
||||
'k8sBaseList',
|
||||
entity,
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
);
|
||||
}, [
|
||||
selectedTime,
|
||||
entity,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
]);
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey,
|
||||
queryFn: ({ signal }) => {
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
return fetchListData(
|
||||
{
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters: queryFilters,
|
||||
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
|
||||
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
|
||||
orderBy: orderBy || undefined,
|
||||
groupBy: groupBy?.length > 0 ? groupBy : undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
},
|
||||
refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
});
|
||||
|
||||
const pageData = data?.data;
|
||||
const totalCount = data?.total || 0;
|
||||
|
||||
const formattedItemsData = useMemo(
|
||||
() => pageData?.map((item) => renderRowData(item, groupBy)),
|
||||
[pageData, renderRowData, groupBy],
|
||||
);
|
||||
|
||||
const handleTableChange: TableProps<K8sRenderedRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter:
|
||||
| SorterResult<K8sRenderedRowData>
|
||||
| SorterResult<K8sRenderedRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
[eventCategory, setCurrentPage, setOrderBy],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
total: totalCount,
|
||||
});
|
||||
}, [eventCategory, totalCount]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sRenderedRowData): void => {
|
||||
if (expandedRowKeys.includes(record.key)) {
|
||||
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
|
||||
} else {
|
||||
setExpandedRowKeys([record.key]);
|
||||
}
|
||||
};
|
||||
|
||||
const openItemInNewTab = (record: K8sRenderedRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set('selectedItem', record.itemKey);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sRenderedRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openItemInNewTab(record);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedItem(record.itemKey);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
};
|
||||
|
||||
const [
|
||||
columnsDefinitions,
|
||||
columnsHidden,
|
||||
] = useInfraMonitoringTableColumnsForPage(entity);
|
||||
|
||||
const hiddenColumnIdsOnList = useMemo(
|
||||
() =>
|
||||
columnsDefinitions
|
||||
.filter(
|
||||
(col) =>
|
||||
(groupBy?.length > 0 && col.behavior === 'hidden-on-expand') ||
|
||||
(!groupBy?.length && col.behavior === 'hidden-on-collapse'),
|
||||
)
|
||||
.map((col) => col.id),
|
||||
[columnsDefinitions, groupBy?.length],
|
||||
);
|
||||
|
||||
const mapDefaultSort = useCallback(
|
||||
(
|
||||
tableColumn: ColumnType<K8sRenderedRowData>,
|
||||
): ColumnType<K8sRenderedRowData> => {
|
||||
if (tableColumn.key === initialOrderBy?.columnName) {
|
||||
return {
|
||||
...tableColumn,
|
||||
defaultSortOrder: initialOrderBy?.order === 'asc' ? 'ascend' : 'descend',
|
||||
};
|
||||
}
|
||||
|
||||
return tableColumn;
|
||||
},
|
||||
[initialOrderBy?.columnName, initialOrderBy?.order],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
tableColumns
|
||||
.filter(
|
||||
(c) =>
|
||||
!hiddenColumnIdsOnList.includes(c.key?.toString() || '') &&
|
||||
!columnsHidden.includes(c.key?.toString() || ''),
|
||||
)
|
||||
.map(mapDefaultSort),
|
||||
[columnsHidden, hiddenColumnIdsOnList, mapDefaultSort, tableColumns],
|
||||
);
|
||||
|
||||
const isGroupedByAttribute = groupBy.length > 0;
|
||||
|
||||
const expandedRowRender = (record: K8sRenderedRowData): JSX.Element => (
|
||||
<K8sExpandedRow<T>
|
||||
record={record}
|
||||
entity={entity}
|
||||
tableColumns={tableColumns}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={renderRowData}
|
||||
/>
|
||||
);
|
||||
|
||||
const expandRowIconRenderer = ({
|
||||
expanded,
|
||||
onExpand,
|
||||
record,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
onExpand: (
|
||||
record: K8sRenderedRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sRenderedRowData;
|
||||
}): JSX.Element | null => {
|
||||
if (!isGroupedByAttribute) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return expanded ? (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronDown size={14} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const onPaginationChange = (page: number, pageSize: number): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState = isLoading;
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sHeader
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={entity}
|
||||
showAutoRefresh={!selectedItem}
|
||||
/>
|
||||
{isError && (
|
||||
<Typography>{data?.error?.toString() || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
|
||||
<Table
|
||||
className={styles.k8sListTable}
|
||||
dataSource={showTableLoadingState ? [] : formattedItemsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: onPaginationChange,
|
||||
}}
|
||||
loading={{
|
||||
spinning: showTableLoadingState,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
locale={{
|
||||
emptyText: showTableLoadingState ? null : (
|
||||
<div className={styles.noFilteredHostsMessageContainer}>
|
||||
<div className={styles.noFilteredHostsMessageContent}>
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className={styles.emptyStateSvg}
|
||||
/>
|
||||
|
||||
<Typography.Text className={styles.noFilteredHostsMessage}>
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: styles.clickableRow,
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
expandIcon: expandRowIconRenderer,
|
||||
expandedRowKeys,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
.drawer {
|
||||
--dialog-description-padding: 0px 0px var(--spacing-8) 0px;
|
||||
}
|
||||
|
||||
.columnItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.columnsTitle {
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--periscope-font-size-small, 11px);
|
||||
font-weight: var(--periscope-font-weight-medium, 500);
|
||||
text-transform: uppercase;
|
||||
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
|
||||
&:not(:first-of-type) {
|
||||
border-top: 1px solid var(--l2-border);
|
||||
}
|
||||
}
|
||||
|
||||
.columnsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.columnItem {
|
||||
justify-content: flex-start !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.horizontalDivider {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Button, DrawerWrapper } from '@signozhq/ui';
|
||||
|
||||
import { K8sCategory } from '../constants';
|
||||
import {
|
||||
useInfraMonitoringTableColumnsForPage,
|
||||
useInfraMonitoringTableColumnsStore,
|
||||
} from './useInfraMonitoringTableColumnsStore';
|
||||
|
||||
import styles from './K8sFiltersSidePanel.module.scss';
|
||||
|
||||
function K8sFiltersSidePanel({
|
||||
open,
|
||||
onClose,
|
||||
entity,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
entity: K8sCategory;
|
||||
}): JSX.Element {
|
||||
const addColumn = useInfraMonitoringTableColumnsStore(
|
||||
(state) => state.addColumn,
|
||||
);
|
||||
const removeColumn = useInfraMonitoringTableColumnsStore(
|
||||
(state) => state.removeColumn,
|
||||
);
|
||||
|
||||
const [columns, columnsHidden] = useInfraMonitoringTableColumnsForPage(entity);
|
||||
|
||||
const drawerContent = (
|
||||
<>
|
||||
<div className={styles.columnsTitle}>Added Columns (Click to remove)</div>
|
||||
|
||||
<div className={styles.columnsList}>
|
||||
{columns
|
||||
.filter(
|
||||
(column) =>
|
||||
!columnsHidden.includes(column.id) &&
|
||||
column.behavior !== 'hidden-on-collapse',
|
||||
)
|
||||
.map((column) => (
|
||||
<div className={styles.columnItem} key={column.value}>
|
||||
{/*<GripVertical size={16} /> TODO: Add support back when update the table component */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="none"
|
||||
className={styles.columnItem}
|
||||
disabled={!column.canBeHidden}
|
||||
data-testid={`remove-column-${column.id}`}
|
||||
onClick={(): void => removeColumn(entity, column.id)}
|
||||
>
|
||||
{column.label}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.horizontalDivider} />
|
||||
|
||||
<div className={styles.columnsTitle}>Other Columns (Click to add)</div>
|
||||
|
||||
<div className={styles.columnsList}>
|
||||
{columns
|
||||
.filter((column) => columnsHidden.includes(column.id))
|
||||
.map((column) => (
|
||||
<div className={styles.columnItem} key={column.value}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="none"
|
||||
className={styles.columnItem}
|
||||
data-can-be-added="true"
|
||||
data-testid={`add-column-${column.id}`}
|
||||
onClick={(): void => addColumn(entity, column.id)}
|
||||
tabIndex={0}
|
||||
>
|
||||
{column.label}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title="Columns"
|
||||
direction="right"
|
||||
showCloseButton
|
||||
showOverlay={false}
|
||||
className={styles.drawer}
|
||||
>
|
||||
{drawerContent}
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sFiltersSidePanel;
|
||||
@@ -1,80 +0,0 @@
|
||||
.k8sListControls {
|
||||
padding: var(--spacing-4);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
|
||||
:global(.ant-select-selector) {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border) !important;
|
||||
background-color: var(--l2-background) !important;
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:global(.ant-tag .ant-typography) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.k8sListControlsLeft {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
|
||||
.k8sQbSearchContainer {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
max-width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
.k8sAttributeSearchContainer {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
max-width: 40%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.groupByLabel {
|
||||
min-width: max-content;
|
||||
font-size: var(--periscope-font-size-base, 13px);
|
||||
font-weight: var(--periscope-font-weight-regular, 400);
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-right: none;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
|
||||
display: flex;
|
||||
height: 32px;
|
||||
padding: var(--spacing-3) var(--spacing-3) var(--spacing-3) var(--spacing-4);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.groupBySelect {
|
||||
:global(.ant-select-selector) {
|
||||
border-left: none;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.k8sListControlsRight {
|
||||
min-width: 240px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Select } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { SlidersHorizontal } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { GetK8sEntityToAggregateAttribute, K8sCategory } from '../constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringFiltersK8s,
|
||||
useInfraMonitoringGroupBy,
|
||||
} from '../hooks';
|
||||
import K8sFiltersSidePanel from './K8sFiltersSidePanel';
|
||||
|
||||
import styles from './K8sHeader.module.scss';
|
||||
|
||||
interface K8sHeaderProps {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
entity: K8sCategory;
|
||||
showAutoRefresh: boolean;
|
||||
}
|
||||
|
||||
function K8sHeader({
|
||||
controlListPrefix,
|
||||
entity,
|
||||
showAutoRefresh,
|
||||
}: K8sHeaderProps): JSX.Element {
|
||||
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
|
||||
|
||||
const currentQuery = initialQueriesMap[DataSource.METRICS];
|
||||
|
||||
const updatedCurrentQuery = useMemo(() => {
|
||||
let { filters } = currentQuery.builder.queryData[0];
|
||||
if (urlFilters) {
|
||||
filters = urlFilters;
|
||||
}
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||
},
|
||||
filters,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}, [currentQuery, urlFilters]);
|
||||
|
||||
const query = useMemo(
|
||||
() => updatedCurrentQuery?.builder?.queryData[0] || null,
|
||||
[updatedCurrentQuery],
|
||||
);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const [, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const handleChangeTagFilters = useCallback(
|
||||
(value: IBuilderQuery['filters']) => {
|
||||
setUrlFilters(value || null);
|
||||
handleChangeQueryData('filters', value);
|
||||
setCurrentPage(1);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
});
|
||||
}
|
||||
},
|
||||
[handleChangeQueryData, setCurrentPage, setUrlFilters],
|
||||
);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: GetK8sEntityToAggregateAttribute(
|
||||
entity,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true,
|
||||
entity,
|
||||
);
|
||||
|
||||
const groupByOptions = useMemo(
|
||||
() =>
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
[groupByFiltersData],
|
||||
);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
const newGroupBy = [];
|
||||
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const element = (value[index] as unknown) as string;
|
||||
|
||||
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||
(k) => k.key === element,
|
||||
);
|
||||
|
||||
if (key) {
|
||||
newGroupBy.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset pagination on switching to groupBy
|
||||
setCurrentPage(1);
|
||||
setGroupBy(newGroupBy);
|
||||
|
||||
logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
});
|
||||
},
|
||||
[groupByFiltersData, setCurrentPage, setGroupBy],
|
||||
);
|
||||
|
||||
const onClickOutside = useCallback(() => {
|
||||
setIsFiltersSidePanelOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.k8sListControls}>
|
||||
<div className={styles.k8sListControlsLeft}>
|
||||
{controlListPrefix}
|
||||
|
||||
<div className={styles.k8sQbSearchContainer}>
|
||||
<QueryBuilderSearch
|
||||
query={query as IBuilderQuery}
|
||||
onChange={handleChangeTagFilters}
|
||||
isInfraMonitoring
|
||||
disableNavigationShortcuts
|
||||
entity={entity}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.k8sAttributeSearchContainer}>
|
||||
<div className={styles.groupByLabel}> Group by </div>
|
||||
<Select
|
||||
className={styles.groupBySelect}
|
||||
loading={isLoadingGroupByFilters}
|
||||
mode="multiple"
|
||||
value={groupBy}
|
||||
allowClear
|
||||
maxTagCount="responsive"
|
||||
placeholder="Search for attribute"
|
||||
style={{ width: '100%' }}
|
||||
options={groupByOptions}
|
||||
onChange={handleGroupByChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.k8sListControlsRight}>
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="none"
|
||||
disabled={groupBy?.length > 0}
|
||||
data-testid="k8s-list-filters-button"
|
||||
onClick={(): void => setIsFiltersSidePanelOpen(true)}
|
||||
>
|
||||
<SlidersHorizontal size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<K8sFiltersSidePanel
|
||||
open={isFiltersSidePanelOpen}
|
||||
entity={entity}
|
||||
onClose={onClickOutside}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sHeader;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,113 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
export interface IEntityColumn {
|
||||
label: string;
|
||||
value: string;
|
||||
id: string;
|
||||
defaultVisibility: boolean;
|
||||
canBeHidden: boolean;
|
||||
behavior: 'hidden-on-expand' | 'hidden-on-collapse' | 'always-visible';
|
||||
}
|
||||
|
||||
export interface IInfraMonitoringTableColumnsStore {
|
||||
columns: Record<string, IEntityColumn[]>;
|
||||
columnsHidden: Record<string, string[]>;
|
||||
addColumn: (page: string, columnId: string) => void;
|
||||
removeColumn: (page: string, columnId: string) => void;
|
||||
initializePageColumns: (page: string, columns: IEntityColumn[]) => void;
|
||||
}
|
||||
|
||||
export const useInfraMonitoringTableColumnsStore = create<IInfraMonitoringTableColumnsStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
columns: {},
|
||||
columnsHidden: {},
|
||||
addColumn: (page, columnId): void => {
|
||||
const state = get();
|
||||
const columnDefinition = state.columns[page]?.find(
|
||||
(c) => c.id === columnId,
|
||||
);
|
||||
|
||||
if (!columnDefinition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!columnDefinition.canBeHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
const columnsHidden = state.columnsHidden[page];
|
||||
|
||||
if (columnsHidden.includes(columnId)) {
|
||||
set({
|
||||
columnsHidden: {
|
||||
...state.columnsHidden,
|
||||
[page]: columnsHidden.filter((id) => id !== columnId),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
removeColumn: (page, columnId): void => {
|
||||
const state = get();
|
||||
const columnDefinition = state.columns[page]?.find(
|
||||
(c) => c.id === columnId,
|
||||
);
|
||||
|
||||
if (!columnDefinition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!columnDefinition.canBeHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
const columnsHidden = state.columnsHidden[page];
|
||||
|
||||
if (!columnsHidden.includes(columnId)) {
|
||||
set({
|
||||
columnsHidden: {
|
||||
...state.columnsHidden,
|
||||
[page]: [...columnsHidden, columnId],
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
initializePageColumns: (page, columns): void => {
|
||||
const state = get();
|
||||
|
||||
set({
|
||||
columns: {
|
||||
...state.columns,
|
||||
[page]: columns,
|
||||
},
|
||||
});
|
||||
|
||||
if (state.columnsHidden[page] === undefined) {
|
||||
set({
|
||||
columnsHidden: {
|
||||
...state.columnsHidden,
|
||||
[page]: columns
|
||||
.filter((c) => c.defaultVisibility === false)
|
||||
.map((c) => c.id),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: '@signoz/infra-monitoring-columns',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringTableColumnsForPage = (
|
||||
page: string,
|
||||
): [columns: IEntityColumn[], columnsHidden: string[]] => {
|
||||
const state = useInfraMonitoringTableColumnsStore((s) => s.columns);
|
||||
const columnsHidden = useInfraMonitoringTableColumnsStore(
|
||||
(s) => s.columnsHidden,
|
||||
);
|
||||
|
||||
return [state[page] ?? [], columnsHidden[page] ?? []];
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
.itemDataGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.itemDataGroupTagItem {
|
||||
display: block !important;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { Badge } from '@signozhq/ui';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import styles from './utils.module.scss';
|
||||
|
||||
const dotToUnder: Record<string, string> = {
|
||||
'k8s.node.name': 'k8s_node_name',
|
||||
'k8s.cluster.name': 'k8s_cluster_name',
|
||||
'k8s.node.uid': 'k8s_node_uid',
|
||||
'k8s.cronjob.name': 'k8s_cronjob_name',
|
||||
'k8s.daemonset.name': 'k8s_daemonset_name',
|
||||
'k8s.deployment.name': 'k8s_deployment_name',
|
||||
'k8s.job.name': 'k8s_job_name',
|
||||
'k8s.namespace.name': 'k8s_namespace_name',
|
||||
'k8s.pod.name': 'k8s_pod_name',
|
||||
'k8s.pod.uid': 'k8s_pod_uid',
|
||||
'k8s.statefulset.name': 'k8s_statefulset_name',
|
||||
'k8s.persistentvolumeclaim.name': 'k8s_persistentvolumeclaim_name',
|
||||
};
|
||||
|
||||
export function getGroupedByMeta<T extends { meta: Record<string, string> }>(
|
||||
itemData: T,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof itemData.meta;
|
||||
result[rawKey] = itemData.meta[metaKey] ?? '';
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getRowKey<T extends { meta: Record<string, string> }>(
|
||||
itemData: T,
|
||||
getItemIdentifier: () => string,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): string {
|
||||
const nodeIdentifier = getItemIdentifier();
|
||||
|
||||
if (groupBy.length === 0) {
|
||||
return nodeIdentifier || JSON.stringify(itemData.meta);
|
||||
}
|
||||
|
||||
const groupedMeta = getGroupedByMeta(itemData, groupBy);
|
||||
const groupKey = Object.values(groupedMeta).join('-');
|
||||
|
||||
if (groupKey && nodeIdentifier) {
|
||||
return `${groupKey}-${nodeIdentifier}`;
|
||||
}
|
||||
if (groupKey) {
|
||||
return groupKey;
|
||||
}
|
||||
if (nodeIdentifier) {
|
||||
return nodeIdentifier;
|
||||
}
|
||||
|
||||
return JSON.stringify(itemData.meta);
|
||||
}
|
||||
|
||||
export function getGroupByEl<T extends { meta: Record<string, string> }>(
|
||||
itemData: T,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): React.ReactNode {
|
||||
const groupByValues: string[] = [];
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
|
||||
// Choose mapped key if present, otherwise use rawKey
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof itemData.meta;
|
||||
const value = itemData.meta[metaKey] || '-';
|
||||
|
||||
groupByValues.push(value);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.itemDataGroup}>
|
||||
{groupByValues.map((value) => (
|
||||
<Badge
|
||||
key={value}
|
||||
color="secondary"
|
||||
className={styles.itemDataGroupTagItem}
|
||||
>
|
||||
{value === '' ? '<no-value>' : value}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { K8sClustersData } from 'api/infraMonitoring/getK8sClustersList';
|
||||
|
||||
export type ClusterDetailsProps = {
|
||||
cluster: K8sClustersData | null;
|
||||
isModalTimeSelection: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -0,0 +1,624 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||
import type { RadioChangeEvent } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { K8sClustersData } from 'api/infraMonitoring/getK8sClustersList';
|
||||
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
|
||||
import {
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import {
|
||||
BarChart2,
|
||||
ChevronsLeftRight,
|
||||
Compass,
|
||||
DraftingCompass,
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import ClusterEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
import ClusterLogs from '../../EntityDetailsUtils/EntityLogs';
|
||||
import ClusterMetrics from '../../EntityDetailsUtils/EntityMetrics';
|
||||
import ClusterTraces from '../../EntityDetailsUtils/EntityTraces';
|
||||
import { ClusterDetailsProps } from './ClusterDetails.interfaces';
|
||||
import { clusterWidgetInfo, getClusterMetricsQueryPayload } from './constants';
|
||||
|
||||
import '../../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
|
||||
function ClusterDetails({
|
||||
cluster,
|
||||
onClose,
|
||||
isModalTimeSelection,
|
||||
}: ClusterDetailsProps): JSX.Element {
|
||||
const { maxTime, minTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
|
||||
minTime,
|
||||
]);
|
||||
const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
||||
startTime: startMs,
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [
|
||||
tracesFiltersParam,
|
||||
setTracesFiltersParam,
|
||||
] = useInfraMonitoringTracesFilters();
|
||||
const [
|
||||
eventsFiltersParam,
|
||||
setEventsFiltersParam,
|
||||
] = useInfraMonitoringEventsFilters();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const initialFilters = useMemo(() => {
|
||||
const filters =
|
||||
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
|
||||
if (filters) {
|
||||
return filters;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_cluster_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster?.meta.k8s_cluster_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [
|
||||
cluster?.meta.k8s_cluster_name,
|
||||
selectedView,
|
||||
logFiltersParam,
|
||||
tracesFiltersParam,
|
||||
]);
|
||||
|
||||
const initialEventsFilters = useMemo(() => {
|
||||
if (eventsFiltersParam) {
|
||||
return eventsFiltersParam;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.kind--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: 'Cluster',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster?.meta.k8s_cluster_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [cluster?.meta.k8s_cluster_name, eventsFiltersParam]);
|
||||
|
||||
const [logsAndTracesFilters, setLogsAndTracesFilters] = useState<
|
||||
IBuilderQuery['filters']
|
||||
>(initialFilters);
|
||||
|
||||
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
|
||||
initialEventsFilters,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (cluster) {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
}
|
||||
}, [cluster]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogsAndTracesFilters(initialFilters);
|
||||
setEventsFilters(initialEventsFilters);
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
}, [selectedTime, minTime, maxTime]);
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
setLogFiltersParam(null);
|
||||
setTracesFiltersParam(null);
|
||||
setEventsFiltersParam(null);
|
||||
logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
view: e.target.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 / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
interval,
|
||||
view: selectedView,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeLogFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogsAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
|
||||
);
|
||||
const paginationFilter = value?.items?.find(
|
||||
(item) => item.key?.key === 'id',
|
||||
);
|
||||
const newFilters = value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
);
|
||||
|
||||
if (newFilters && newFilters?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
view: InfraMonitoringEvents.LogsView,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(newFilters || []),
|
||||
...(paginationFilter ? [paginationFilter] : []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setLogFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeTracesFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogsAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(value?.items?.filter(
|
||||
(item) => item.key?.key !== QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setTracesFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeEventsFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setEventsFilters((prevFilters) => {
|
||||
const clusterKindFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
);
|
||||
const clusterNameFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
view: InfraMonitoringEvents.EventsView,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
clusterKindFilter,
|
||||
clusterNameFilter,
|
||||
...(value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setEventsFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleExplorePagesRedirect = (): void => {
|
||||
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: InfraMonitoringEvents.Cluster,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const filtersWithoutPagination = {
|
||||
...logsAndTracesFilters,
|
||||
items:
|
||||
logsAndTracesFilters?.items?.filter((item) => item.key?.key !== 'id') ||
|
||||
[],
|
||||
};
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.logs,
|
||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||
filters: filtersWithoutPagination,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
aggregateOperator: TracesAggregatorOperator.NOOP,
|
||||
filters: logsAndTracesFilters,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
setSelectedView(VIEW_TYPES.METRICS);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="70%"
|
||||
title={
|
||||
<>
|
||||
<Divider type="vertical" />
|
||||
<Typography.Text className="title">
|
||||
{cluster?.meta.k8s_cluster_name}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!cluster}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="entity-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{cluster && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Cluster Name
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="values-row">
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={cluster.meta.k8s_cluster_name}>
|
||||
{cluster.meta.k8s_cluster_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="views-tabs-container">
|
||||
<Radio.Group
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.METRICS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<BarChart2 size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.LOGS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.LOGS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.TRACES ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.TRACES}
|
||||
>
|
||||
<div className="view-title">
|
||||
<DraftingCompass size={14} />
|
||||
Traces
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.EVENTS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.EVENTS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Events
|
||||
</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
|
||||
{(selectedView === VIEW_TYPES.LOGS ||
|
||||
selectedView === VIEW_TYPES.TRACES) && (
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{selectedView === VIEW_TYPES.METRICS && (
|
||||
<ClusterMetrics<K8sClustersData>
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
entity={cluster}
|
||||
entityWidgetInfo={clusterWidgetInfo}
|
||||
getEntityQueryPayload={getClusterMetricsQueryPayload}
|
||||
category={K8sCategory.CLUSTERS}
|
||||
queryKey="clusterMetrics"
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<ClusterLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logsAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="clusterLogs"
|
||||
category={K8sCategory.CLUSTERS}
|
||||
queryKeyFilters={[QUERY_KEYS.K8S_CLUSTER_NAME]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<ClusterTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||
tracesFilters={logsAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="clusterTraces"
|
||||
category={InfraMonitoringEvents.Cluster}
|
||||
queryKeyFilters={[QUERY_KEYS.K8S_CLUSTER_NAME]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.EVENTS && (
|
||||
<ClusterEvents
|
||||
timeRange={modalTimeRange}
|
||||
handleChangeEventFilters={handleChangeEventsFilters}
|
||||
filters={eventsFilters}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
category={K8sCategory.CLUSTERS}
|
||||
queryKey="clusterEvents"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClusterDetails;
|
||||
@@ -1,57 +1,11 @@
|
||||
import { K8sClustersData } from 'api/infraMonitoring/getK8sClustersList';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
createFilterItem,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from '../Base/K8sBaseDetails';
|
||||
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
|
||||
import { K8sClusterData } from './api';
|
||||
|
||||
export const k8sClusterGetSelectedItemFilters = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_cluster_name',
|
||||
key: {
|
||||
key: 'k8s_cluster_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const k8sClusterDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sClusterData>[] = [
|
||||
{ label: 'Cluster Name', getValue: (p): string => p.meta.k8s_cluster_name },
|
||||
];
|
||||
|
||||
export const k8sClusterInitialFilters = [QUERY_KEYS.K8S_CLUSTER_NAME];
|
||||
|
||||
export const k8sClusterInitialEventsFilter = (
|
||||
item: K8sClusterData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Cluster'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_cluster_name),
|
||||
];
|
||||
|
||||
export const k8sClusterInitialLogTracesFilter = (
|
||||
item: K8sClusterData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_CLUSTER_NAME, item.meta.k8s_cluster_name),
|
||||
];
|
||||
|
||||
export const k8sClusterGetEntityName = (item: K8sClusterData): string =>
|
||||
item.meta.k8s_cluster_name;
|
||||
|
||||
export const clusterWidgetInfo = [
|
||||
{
|
||||
title: 'CPU Usage, allocatable',
|
||||
@@ -88,7 +42,7 @@ export const clusterWidgetInfo = [
|
||||
];
|
||||
|
||||
export const getClusterMetricsQueryPayload = (
|
||||
cluster: K8sClusterData,
|
||||
cluster: K8sClustersData,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
@@ -0,0 +1,3 @@
|
||||
import ClusterDetails from './ClusterDetails';
|
||||
|
||||
export default ClusterDetails;
|
||||
@@ -0,0 +1,17 @@
|
||||
.infra-monitoring-container {
|
||||
.clusters-list-table {
|
||||
.expanded-table-container {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
min-width: 223px !important;
|
||||
max-width: 223px !important;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,116 +1,693 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Spin,
|
||||
Table,
|
||||
TableColumnType as ColumnType,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { K8sClustersListPayload } from 'api/infraMonitoring/getK8sClustersList';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useGetK8sClustersList } from 'hooks/infraMonitoring/useGetK8sClustersList';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseFilters, K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sCategory } from '../constants';
|
||||
import { getK8sClustersList, K8sClusterData } from './api';
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import {
|
||||
clusterWidgetInfo,
|
||||
getClusterMetricsQueryPayload,
|
||||
k8sClusterDetailsMetadataConfig,
|
||||
k8sClusterGetEntityName,
|
||||
k8sClusterGetSelectedItemFilters,
|
||||
k8sClusterInitialEventsFilter,
|
||||
k8sClusterInitialFilters,
|
||||
k8sClusterInitialLogTracesFilter,
|
||||
} from './constants';
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
K8sCategory,
|
||||
} from '../constants';
|
||||
import {
|
||||
k8sClustersColumns,
|
||||
k8sClustersColumnsConfig,
|
||||
k8sClustersRenderRowData,
|
||||
} from './table';
|
||||
useInfraMonitoringClusterName,
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
} from '../hooks';
|
||||
import K8sHeader from '../K8sHeader';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
import { usePageSize } from '../utils';
|
||||
import ClusterDetails from './ClusterDetails';
|
||||
import {
|
||||
defaultAddedColumns,
|
||||
formatDataForTable,
|
||||
getK8sClustersListColumns,
|
||||
getK8sClustersListQuery,
|
||||
K8sClustersRowData,
|
||||
} from './utils';
|
||||
|
||||
import '../InfraMonitoringK8s.styles.scss';
|
||||
import './K8sClustersList.styles.scss';
|
||||
|
||||
function K8sClustersList({
|
||||
controlListPrefix,
|
||||
isFiltersVisible,
|
||||
handleFilterVisibilityChange,
|
||||
quickFiltersLastUpdated,
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
isFiltersVisible: boolean;
|
||||
handleFilterVisibilityChange: () => void;
|
||||
quickFiltersLastUpdated: number;
|
||||
}): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
const [
|
||||
selectedClusterName,
|
||||
setselectedClusterName,
|
||||
] = useInfraMonitoringClusterName();
|
||||
|
||||
const [filtersInitialised, setFiltersInitialised] = useState(false);
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
|
||||
const { pageSize, setPageSize } = usePageSize(K8sCategory.CLUSTERS);
|
||||
|
||||
const [
|
||||
selectedRowData,
|
||||
setSelectedRowData,
|
||||
] = useState<K8sClustersRowData | null>(null);
|
||||
|
||||
const [groupByOptions, setGroupByOptions] = useState<
|
||||
{ value: string; label: string }[]
|
||||
>([]);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const queryFilters = useMemo(
|
||||
() =>
|
||||
currentQuery?.builder?.queryData[0]?.filters || {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
[currentQuery?.builder?.queryData],
|
||||
);
|
||||
|
||||
// Reset pagination every time quick filters are changed
|
||||
useEffect(() => {
|
||||
if (quickFiltersLastUpdated !== -1) {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
}, [quickFiltersLastUpdated, setCurrentPage]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
filters.orderBy ||= {
|
||||
columnName: 'cpu',
|
||||
order: 'desc',
|
||||
};
|
||||
const createFiltersForSelectedRowData = (
|
||||
selectedRowData: K8sClustersRowData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): IBuilderQuery['filters'] => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...queryFilters.items],
|
||||
op: 'and',
|
||||
};
|
||||
|
||||
const response = await getK8sClustersList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
if (!selectedRowData) {
|
||||
return baseFilters;
|
||||
}
|
||||
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
};
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
const { groupedByMeta } = selectedRowData;
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: K8sClusterData | null; error?: string | null }> => {
|
||||
const response = await getK8sClustersList(
|
||||
{
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
for (const key of groupBy) {
|
||||
baseFilters.items.push({
|
||||
key: {
|
||||
key: key.key,
|
||||
type: null,
|
||||
},
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
op: '=',
|
||||
value: groupedByMeta[key.key],
|
||||
id: key.key,
|
||||
});
|
||||
}
|
||||
|
||||
const records = response.payload?.data.records || [];
|
||||
return baseFilters;
|
||||
};
|
||||
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
const fetchGroupedByRowDataQuery = useMemo(() => {
|
||||
if (!selectedRowData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseQuery = getK8sClustersListQuery();
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||
|
||||
return {
|
||||
...baseQuery,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
filters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(() => {
|
||||
// be careful with what you serialize from selectedRowData
|
||||
// since it's react node, it could contain circular references
|
||||
const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta);
|
||||
if (selectedClusterName) {
|
||||
return [
|
||||
'clusterList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'clusterList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
queryFilters,
|
||||
orderBy,
|
||||
selectedClusterName,
|
||||
minTime,
|
||||
maxTime,
|
||||
selectedRowData,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
isLoading: isLoadingGroupedByRowData,
|
||||
isError: isErrorGroupedByRowData,
|
||||
refetch: fetchGroupedByRowData,
|
||||
} = useGetK8sClustersList(
|
||||
fetchGroupedByRowDataQuery as K8sClustersListPayload,
|
||||
{
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: GetK8sEntityToAggregateAttribute(
|
||||
K8sCategory.CLUSTERS,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true,
|
||||
K8sCategory.NODES,
|
||||
);
|
||||
|
||||
const query = useMemo(() => {
|
||||
const baseQuery = getK8sClustersListQuery();
|
||||
const queryPayload = {
|
||||
...baseQuery,
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters: queryFilters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
if (groupBy.length > 0) {
|
||||
queryPayload.groupBy = groupBy;
|
||||
}
|
||||
return queryPayload;
|
||||
}, [pageSize, currentPage, queryFilters, minTime, maxTime, orderBy, groupBy]);
|
||||
|
||||
const formattedGroupedByClustersData = useMemo(
|
||||
() =>
|
||||
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const nestedClustersData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) {
|
||||
return [];
|
||||
}
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedClusterName) {
|
||||
return [
|
||||
'clusterList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'clusterList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedClusterName,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sClustersList(
|
||||
query as K8sClustersListPayload,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const clustersData = useMemo(() => data?.payload?.data?.records || [], [data]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const formattedClustersData = useMemo(
|
||||
() => formatDataForTable(clustersData, groupBy),
|
||||
[clustersData, groupBy],
|
||||
);
|
||||
|
||||
const columns = useMemo(() => getK8sClustersListColumns(groupBy), [groupBy]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sClustersRowData): void => {
|
||||
setSelectedRowData(record);
|
||||
|
||||
if (expandedRowKeys.includes(record.key)) {
|
||||
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
|
||||
} else {
|
||||
setExpandedRowKeys([record.key]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRowData) {
|
||||
fetchGroupedByRowData();
|
||||
}
|
||||
}, [selectedRowData, fetchGroupedByRowData]);
|
||||
|
||||
const handleTableChange: TableProps<K8sClustersRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter:
|
||||
| SorterResult<K8sClustersRowData>
|
||||
| SorterResult<K8sClustersRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
[setCurrentPage, setOrderBy],
|
||||
);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const handleFiltersChange = useCallback(
|
||||
(value: IBuilderQuery['filters']): void => {
|
||||
handleChangeQueryData('filters', value);
|
||||
if (filtersInitialised) {
|
||||
setCurrentPage(1);
|
||||
} else {
|
||||
setFiltersInitialised(true);
|
||||
}
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
total: data?.payload?.data?.total,
|
||||
});
|
||||
}, [data?.payload?.data?.total]);
|
||||
|
||||
const selectedClusterData = useMemo(() => {
|
||||
if (!selectedClusterName) {
|
||||
return null;
|
||||
}
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the cluster from the formatted grouped by clusters data
|
||||
return (
|
||||
nestedClustersData.find(
|
||||
(cluster) => cluster.meta.k8s_cluster_name === selectedClusterName,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
// If not grouped by, return the cluster from the clusters data
|
||||
return (
|
||||
clustersData.find(
|
||||
(cluster) => cluster.meta.k8s_cluster_name === selectedClusterName,
|
||||
) || null
|
||||
);
|
||||
}, [selectedClusterName, groupBy.length, clustersData, nestedClustersData]);
|
||||
|
||||
const openClusterInNewTab = (record: K8sClustersRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
|
||||
record.clusterUID,
|
||||
);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sClustersRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openClusterInNewTab(record);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedClusterName(record.clusterUID);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
};
|
||||
|
||||
const nestedColumns = useMemo(() => getK8sClustersListColumns([]), []);
|
||||
|
||||
const isGroupedByAttribute = groupBy.length > 0;
|
||||
|
||||
const handleExpandedRowViewAllClick = (): void => {
|
||||
if (!selectedRowData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||
|
||||
handleFiltersChange(filters);
|
||||
|
||||
setCurrentPage(1);
|
||||
setSelectedRowData(null);
|
||||
setGroupBy([]);
|
||||
setOrderBy(null);
|
||||
};
|
||||
|
||||
const expandedRowRender = (): JSX.Element => (
|
||||
<div className="expanded-table-container">
|
||||
{isErrorGroupedByRowData && (
|
||||
<Typography>{groupedByRowData?.error || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
{isFetchingGroupedByRowData || isLoadingGroupedByRowData ? (
|
||||
<LoadingContainer />
|
||||
) : (
|
||||
<div className="expanded-table">
|
||||
<Table
|
||||
columns={nestedColumns as ColumnType<K8sClustersRowData>[]}
|
||||
dataSource={formattedGroupedByClustersData}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
size="small"
|
||||
loading={{
|
||||
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openClusterInNewTab(record);
|
||||
return;
|
||||
}
|
||||
setselectedClusterName(record.clusterUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{groupedByRowData?.payload?.data?.total &&
|
||||
groupedByRowData?.payload?.data?.total > 10 ? (
|
||||
<div className="expanded-table-footer">
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
className="periscope-btn secondary"
|
||||
onClick={handleExpandedRowViewAllClick}
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const expandRowIconRenderer = ({
|
||||
expanded,
|
||||
onExpand,
|
||||
record,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
onExpand: (
|
||||
record: K8sClustersRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sClustersRowData;
|
||||
}): JSX.Element | null => {
|
||||
if (!isGroupedByAttribute) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return expanded ? (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronDown size={14} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const handleCloseClusterDetail = (): void => {
|
||||
setselectedClusterName(null);
|
||||
};
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
const newGroupBy = [];
|
||||
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const element = (value[index] as unknown) as string;
|
||||
|
||||
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||
(key) => key.key === element,
|
||||
);
|
||||
|
||||
if (key) {
|
||||
newGroupBy.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset pagination on switching to groupBy
|
||||
setCurrentPage(1);
|
||||
setGroupBy(newGroupBy);
|
||||
setExpandedRowKeys([]);
|
||||
logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
},
|
||||
[groupByFiltersData, setCurrentPage, setGroupBy],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupByFiltersData?.payload) {
|
||||
setGroupByOptions(
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
);
|
||||
}
|
||||
}, [groupByFiltersData]);
|
||||
|
||||
const onPaginationChange = (page: number, pageSize: number): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedClustersData.length === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<K8sClusterData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={K8sCategory.CLUSTERS}
|
||||
tableColumnsDefinitions={k8sClustersColumns}
|
||||
tableColumns={k8sClustersColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sClustersRenderRowData}
|
||||
eventCategory={InfraMonitoringEvents.Cluster}
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
isFiltersVisible={isFiltersVisible}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
defaultAddedColumns={defaultAddedColumns}
|
||||
handleFiltersChange={handleFiltersChange}
|
||||
groupByOptions={groupByOptions}
|
||||
isLoadingGroupByFilters={isLoadingGroupByFilters}
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
selectedGroupBy={groupBy}
|
||||
entity={K8sCategory.NODES}
|
||||
showAutoRefresh={!selectedClusterData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
<Table
|
||||
className="k8s-list-table clusters-list-table"
|
||||
dataSource={showTableLoadingState ? [] : formattedClustersData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: onPaginationChange,
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: showTableLoadingState,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
locale={{
|
||||
emptyText: showTableLoadingState ? null : (
|
||||
<div className="no-filtered-hosts-message-container">
|
||||
<div className="no-filtered-hosts-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-filtered-hosts-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
expandIcon: expandRowIconRenderer,
|
||||
expandedRowKeys,
|
||||
}}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<K8sClusterData>
|
||||
category={K8sCategory.CLUSTERS}
|
||||
eventCategory={InfraMonitoringEvents.Cluster}
|
||||
getSelectedItemFilters={k8sClusterGetSelectedItemFilters}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sClusterGetEntityName}
|
||||
getInitialLogTracesFilters={k8sClusterInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sClusterInitialEventsFilter}
|
||||
primaryFilterKeys={k8sClusterInitialFilters}
|
||||
metadataConfig={k8sClusterDetailsMetadataConfig}
|
||||
entityWidgetInfo={clusterWidgetInfo}
|
||||
getEntityQueryPayload={getClusterMetricsQueryPayload}
|
||||
queryKeyPrefix="cluster"
|
||||
<ClusterDetails
|
||||
cluster={selectedClusterData}
|
||||
isModalTimeSelection
|
||||
onClose={handleCloseClusterDetail}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
.entityGroupHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/K8sBaseList';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import { formatBytes, ValidateColumnValueWrapper } from '../commonUtils';
|
||||
import { K8sClusterData, K8sClustersListPayload } from './api';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export interface K8sClustersRowData {
|
||||
key: string;
|
||||
itemKey: string;
|
||||
clusterUID: string;
|
||||
clusterName: React.ReactNode;
|
||||
cpu: React.ReactNode;
|
||||
cpu_allocatable: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
memory_allocatable: React.ReactNode;
|
||||
groupedByMeta?: Record<string, string>;
|
||||
}
|
||||
|
||||
export const k8sClustersColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Cluster Group',
|
||||
value: 'clusterGroup',
|
||||
id: 'clusterGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
value: 'clusterName',
|
||||
id: 'clusterName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Alloc (cores)',
|
||||
value: 'cpu_allocatable',
|
||||
id: 'cpu_allocatable',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Memory Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Memory Alloc (bytes)',
|
||||
value: 'memory_allocatable',
|
||||
id: 'memory_allocatable',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
|
||||
export const getK8sClustersListQuery = (): K8sClustersListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
export const k8sClustersColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> CLUSTER GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'clusterGroup',
|
||||
key: 'clusterGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
},
|
||||
{
|
||||
title: <div>Cluster Name</div>,
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Alloc (cores)</div>,
|
||||
dataIndex: 'cpu_allocatable',
|
||||
key: 'cpu_allocatable',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Memory Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Memory Allocatable</div>,
|
||||
dataIndex: 'memory_allocatable',
|
||||
key: 'memory_allocatable',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sClustersRenderRowData = (
|
||||
cluster: K8sClusterData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(
|
||||
cluster,
|
||||
() =>
|
||||
cluster.clusterUID ||
|
||||
cluster.meta.k8s_cluster_uid ||
|
||||
cluster.meta.k8s_cluster_name,
|
||||
groupBy,
|
||||
),
|
||||
itemKey: cluster.meta.k8s_cluster_name,
|
||||
clusterUID: cluster.clusterUID || cluster.meta.k8s_cluster_uid,
|
||||
clusterName: (
|
||||
<Tooltip title={cluster.meta.k8s_cluster_name}>
|
||||
{cluster.meta.k8s_cluster_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={cluster.cpuUsage}>
|
||||
{cluster.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={cluster.memoryUsage}>
|
||||
{formatBytes(cluster.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_allocatable: (
|
||||
<ValidateColumnValueWrapper value={cluster.cpuAllocatable}>
|
||||
{cluster.cpuAllocatable}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_allocatable: (
|
||||
<ValidateColumnValueWrapper value={cluster.memoryAllocatable}>
|
||||
{formatBytes(cluster.memoryAllocatable)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
clusterGroup: getGroupByEl(cluster, groupBy),
|
||||
...cluster.meta,
|
||||
groupedByMeta: getGroupedByMeta(cluster, groupBy),
|
||||
});
|
||||
206
frontend/src/container/InfraMonitoringK8s/Clusters/utils.tsx
Normal file
206
frontend/src/container/InfraMonitoringK8s/Clusters/utils.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
|
||||
import {
|
||||
K8sClustersData,
|
||||
K8sClustersListPayload,
|
||||
} from 'api/infraMonitoring/getK8sClustersList';
|
||||
import { Group } from 'lucide-react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { formatBytes, ValidateColumnValueWrapper } from '../commonUtils';
|
||||
import { IEntityColumn } from '../utils';
|
||||
|
||||
export const defaultAddedColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
value: 'clusterName',
|
||||
id: 'cluster',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Allocatable (cores)',
|
||||
value: 'cpu_allocatable',
|
||||
id: 'cpu_allocatable',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Mem Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Mem Allocatable',
|
||||
value: 'memory_allocatable',
|
||||
id: 'memory_allocatable',
|
||||
canRemove: false,
|
||||
},
|
||||
];
|
||||
|
||||
export interface K8sClustersRowData {
|
||||
key: string;
|
||||
clusterUID: string;
|
||||
clusterName: React.ReactNode;
|
||||
cpu: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
cpu_allocatable: React.ReactNode;
|
||||
memory_allocatable: React.ReactNode;
|
||||
groupedByMeta?: any;
|
||||
}
|
||||
|
||||
const clusterGroupColumnConfig = {
|
||||
title: (
|
||||
<div className="column-header entity-group-header">
|
||||
<Group size={14} /> CLUSTER GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'clusterGroup',
|
||||
key: 'clusterGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
className: 'column entity-group-header',
|
||||
};
|
||||
|
||||
export const getK8sClustersListQuery = (): K8sClustersListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
const columnsConfig = [
|
||||
{
|
||||
title: <div className="column-header-left">Cluster Name</div>,
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left">CPU Utilization (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left">CPU Allocatable (cores)</div>,
|
||||
dataIndex: 'cpu_allocatable',
|
||||
key: 'cpu_allocatable',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left">Memory Utilization (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left">Memory Allocatable</div>,
|
||||
dataIndex: 'memory_allocatable',
|
||||
key: 'memory_allocatable',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const getK8sClustersListColumns = (
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): ColumnType<K8sClustersRowData>[] => {
|
||||
if (groupBy.length > 0) {
|
||||
const filteredColumns = [...columnsConfig].filter(
|
||||
(column) => column.key !== 'clusterName',
|
||||
);
|
||||
filteredColumns.unshift(clusterGroupColumnConfig);
|
||||
return filteredColumns as ColumnType<K8sClustersRowData>[];
|
||||
}
|
||||
|
||||
return columnsConfig as ColumnType<K8sClustersRowData>[];
|
||||
};
|
||||
|
||||
const dotToUnder: Record<string, keyof K8sClustersData['meta']> = {
|
||||
'k8s.cluster.name': 'k8s_cluster_name',
|
||||
'k8s.cluster.uid': 'k8s_cluster_uid',
|
||||
};
|
||||
|
||||
const getGroupByEle = (
|
||||
cluster: K8sClustersData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): React.ReactNode => {
|
||||
const groupByValues: string[] = [];
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
|
||||
// Choose mapped key if present, otherwise use rawKey
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof cluster.meta;
|
||||
const value = cluster.meta[metaKey];
|
||||
|
||||
groupByValues.push(value);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="pod-group">
|
||||
{groupByValues.map((value) => (
|
||||
<Tag key={value} color={Color.BG_SLATE_400} className="pod-group-tag-item">
|
||||
{value === '' ? '<no-value>' : value}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const formatDataForTable = (
|
||||
data: K8sClustersData[],
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): K8sClustersRowData[] =>
|
||||
data.map((cluster, index) => ({
|
||||
key: index.toString(),
|
||||
clusterUID: cluster.meta.k8s_cluster_name,
|
||||
clusterName: (
|
||||
<Tooltip title={cluster.meta.k8s_cluster_name}>
|
||||
{cluster.meta.k8s_cluster_name}
|
||||
</Tooltip>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={cluster.cpuUsage}>
|
||||
{cluster.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={cluster.memoryUsage}>
|
||||
{formatBytes(cluster.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_allocatable: (
|
||||
<ValidateColumnValueWrapper value={cluster.cpuAllocatable}>
|
||||
{cluster.cpuAllocatable}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_allocatable: (
|
||||
<ValidateColumnValueWrapper value={cluster.memoryAllocatable}>
|
||||
{formatBytes(cluster.memoryAllocatable)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
clusterGroup: getGroupByEle(cluster, groupBy),
|
||||
meta: cluster.meta,
|
||||
...cluster.meta,
|
||||
groupedByMeta: cluster.meta,
|
||||
}));
|
||||
@@ -0,0 +1,7 @@
|
||||
import { K8sDaemonSetsData } from 'api/infraMonitoring/getK8sDaemonSetsList';
|
||||
|
||||
export type DaemonSetDetailsProps = {
|
||||
daemonSet: K8sDaemonSetsData | null;
|
||||
isModalTimeSelection: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -0,0 +1,667 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||
import type { RadioChangeEvent } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import {
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import {
|
||||
BarChart2,
|
||||
ChevronsLeftRight,
|
||||
Compass,
|
||||
DraftingCompass,
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import DaemonSetEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
import DaemonSetLogs from '../../EntityDetailsUtils/EntityLogs';
|
||||
import DaemonSetMetrics from '../../EntityDetailsUtils/EntityMetrics';
|
||||
import DaemonSetTraces from '../../EntityDetailsUtils/EntityTraces';
|
||||
import { QUERY_KEYS } from '../../EntityDetailsUtils/utils';
|
||||
import {
|
||||
daemonSetWidgetInfo,
|
||||
getDaemonSetMetricsQueryPayload,
|
||||
} from './constants';
|
||||
import { DaemonSetDetailsProps } from './DaemonSetDetails.interfaces';
|
||||
|
||||
import '../../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
|
||||
function DaemonSetDetails({
|
||||
daemonSet,
|
||||
onClose,
|
||||
isModalTimeSelection,
|
||||
}: DaemonSetDetailsProps): JSX.Element {
|
||||
const { maxTime, minTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
|
||||
minTime,
|
||||
]);
|
||||
const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
||||
startTime: startMs,
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [
|
||||
tracesFiltersParam,
|
||||
setTracesFiltersParam,
|
||||
] = useInfraMonitoringTracesFilters();
|
||||
const [
|
||||
eventsFiltersParam,
|
||||
setEventsFiltersParam,
|
||||
] = useInfraMonitoringEventsFilters();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const initialFilters = useMemo(() => {
|
||||
const filters =
|
||||
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
|
||||
if (filters) {
|
||||
return filters;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_daemonSet_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet?.meta.k8s_daemonset_name || '',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_daemonSet_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet?.meta.k8s_namespace_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [
|
||||
daemonSet?.meta.k8s_daemonset_name,
|
||||
daemonSet?.meta.k8s_namespace_name,
|
||||
selectedView,
|
||||
logFiltersParam,
|
||||
tracesFiltersParam,
|
||||
]);
|
||||
|
||||
const initialEventsFilters = useMemo(() => {
|
||||
if (eventsFiltersParam) {
|
||||
return eventsFiltersParam;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.kind--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: 'DaemonSet',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet?.meta.k8s_daemonset_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [daemonSet?.meta.k8s_daemonset_name, eventsFiltersParam]);
|
||||
|
||||
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
|
||||
IBuilderQuery['filters']
|
||||
>(initialFilters);
|
||||
|
||||
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
|
||||
initialEventsFilters,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (daemonSet) {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
});
|
||||
}
|
||||
}, [daemonSet]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogAndTracesFilters(initialFilters);
|
||||
setEventsFilters(initialEventsFilters);
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
}, [selectedTime, minTime, maxTime]);
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
setLogFiltersParam(null);
|
||||
setTracesFiltersParam(null);
|
||||
setEventsFiltersParam(null);
|
||||
logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
view: e.target.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 / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
interval,
|
||||
view: selectedView,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeLogFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_DAEMON_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||
item.key?.key ?? '',
|
||||
),
|
||||
);
|
||||
const paginationFilter = value?.items?.find(
|
||||
(item) => item.key?.key === 'id',
|
||||
);
|
||||
const newFilters = value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== 'id' &&
|
||||
item.key?.key !== QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
||||
);
|
||||
|
||||
if (newFilters && newFilters?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
view: InfraMonitoringEvents.LogsView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(newFilters || []),
|
||||
...(paginationFilter ? [paginationFilter] : []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setLogFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeTracesFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_DAEMON_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||
item.key?.key ?? '',
|
||||
),
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(value?.items?.filter(
|
||||
(item) => item.key?.key !== QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setTracesFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeEventsFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setEventsFilters((prevFilters) => {
|
||||
const daemonSetKindFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
);
|
||||
const daemonSetNameFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
view: InfraMonitoringEvents.EventsView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: [
|
||||
daemonSetKindFilter,
|
||||
daemonSetNameFilter,
|
||||
...(value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
};
|
||||
|
||||
setEventsFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleExplorePagesRedirect = (): void => {
|
||||
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: InfraMonitoringEvents.DaemonSet,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const filtersWithoutPagination = {
|
||||
...logAndTracesFilters,
|
||||
items: logAndTracesFilters?.items?.filter((item) => item.key?.key !== 'id'),
|
||||
};
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.logs,
|
||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||
filters: filtersWithoutPagination,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
aggregateOperator: TracesAggregatorOperator.NOOP,
|
||||
filters: logAndTracesFilters,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
setSelectedView(VIEW_TYPES.METRICS);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="70%"
|
||||
title={
|
||||
<>
|
||||
<Divider type="vertical" />
|
||||
<Typography.Text className="title">
|
||||
{daemonSet?.meta.k8s_daemonset_name}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!daemonSet}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="entity-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{daemonSet && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Daemonset Name
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Cluster Name
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Namespace Name
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="values-row">
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={daemonSet.meta.k8s_daemonset_name}>
|
||||
{daemonSet.meta.k8s_daemonset_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={daemonSet.meta.k8s_cluster_name}>
|
||||
{daemonSet.meta.k8s_cluster_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={daemonSet.meta.k8s_namespace_name}>
|
||||
{daemonSet.meta.k8s_namespace_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="views-tabs-container">
|
||||
<Radio.Group
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.METRICS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<BarChart2 size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.LOGS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.LOGS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.TRACES ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.TRACES}
|
||||
>
|
||||
<div className="view-title">
|
||||
<DraftingCompass size={14} />
|
||||
Traces
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.EVENTS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.EVENTS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Events
|
||||
</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
|
||||
{(selectedView === VIEW_TYPES.LOGS ||
|
||||
selectedView === VIEW_TYPES.TRACES) && (
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{selectedView === VIEW_TYPES.METRICS && (
|
||||
<DaemonSetMetrics
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
entity={daemonSet}
|
||||
entityWidgetInfo={daemonSetWidgetInfo}
|
||||
getEntityQueryPayload={getDaemonSetMetricsQueryPayload}
|
||||
category={K8sCategory.DAEMONSETS}
|
||||
queryKey="daemonsetMetrics"
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<DaemonSetLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
category={K8sCategory.DAEMONSETS}
|
||||
queryKey="daemonsetLogs"
|
||||
queryKeyFilters={[
|
||||
QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<DaemonSetTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||
tracesFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="daemonsetTraces"
|
||||
category={InfraMonitoringEvents.DaemonSet}
|
||||
queryKeyFilters={[
|
||||
QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.EVENTS && (
|
||||
<DaemonSetEvents
|
||||
timeRange={modalTimeRange}
|
||||
handleChangeEventFilters={handleChangeEventsFilters}
|
||||
filters={eventsFilters}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="daemonsetEvents"
|
||||
category={K8sCategory.DAEMONSETS}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default DaemonSetDetails;
|
||||
@@ -1,72 +1,11 @@
|
||||
import { K8sDaemonSetsData } from 'api/infraMonitoring/getK8sDaemonSetsList';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
createFilterItem,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from '../Base/K8sBaseDetails';
|
||||
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
|
||||
import { K8sDaemonSetsData } from './api';
|
||||
|
||||
export const k8sDaemonSetGetSelectedItemFilters = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_daemonset_name',
|
||||
key: {
|
||||
key: 'k8s_daemonset_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const k8sDaemonSetDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sDaemonSetsData>[] = [
|
||||
{
|
||||
label: 'Daemonset Name',
|
||||
getValue: (p): string => p.meta.k8s_daemonset_name,
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
getValue: (p): string => p.meta.k8s_cluster_name,
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
getValue: (p): string => p.meta.k8s_namespace_name,
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDaemonSetInitialFilters = [
|
||||
QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
];
|
||||
|
||||
export const k8sDaemonSetInitialEventsFilter = (
|
||||
item: K8sDaemonSetsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'DaemonSet'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_daemonset_name),
|
||||
];
|
||||
|
||||
export const k8sDaemonSetInitialLogTracesFilter = (
|
||||
item: K8sDaemonSetsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_DAEMON_SET_NAME, item.meta.k8s_daemonset_name),
|
||||
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
|
||||
];
|
||||
|
||||
export const k8sDaemonSetGetEntityName = (item: K8sDaemonSetsData): string =>
|
||||
item.meta.k8s_daemonset_name;
|
||||
|
||||
export const daemonSetWidgetInfo = [
|
||||
{
|
||||
title: 'CPU usage, request, limits',
|
||||
@@ -0,0 +1,3 @@
|
||||
import DaemonSetDetails from './DaemonSetDetails';
|
||||
|
||||
export default DaemonSetDetails;
|
||||
@@ -0,0 +1,62 @@
|
||||
.infra-monitoring-container {
|
||||
.daemonSets-list-table {
|
||||
.expanded-table-container {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.ant-table-cell.column-progress-bar {
|
||||
min-width: 180px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.daemonset-name-header) {
|
||||
min-width: 250px !important;
|
||||
max-width: 250px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.namespace-name-header) {
|
||||
min-width: 220px !important;
|
||||
max-width: 220px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.med-col) {
|
||||
min-width: 200px !important;
|
||||
max-width: 200px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.small-col) {
|
||||
min-width: 120px !important;
|
||||
max-width: 120px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-daemonsets-list-table {
|
||||
.ant-table-cell {
|
||||
min-width: 220px !important;
|
||||
max-width: 220px !important;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
}
|
||||
@@ -1,116 +1,715 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Spin,
|
||||
Table,
|
||||
TableColumnType as ColumnType,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { K8sDaemonSetsListPayload } from 'api/infraMonitoring/getK8sDaemonSetsList';
|
||||
import classNames from 'classnames';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useGetK8sDaemonSetsList } from 'hooks/infraMonitoring/useGetK8sDaemonSetsList';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseFilters, K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sCategory } from '../constants';
|
||||
import { getK8sDaemonSetsList, K8sDaemonSetsData } from './api';
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import {
|
||||
daemonSetWidgetInfo,
|
||||
getDaemonSetMetricsQueryPayload,
|
||||
k8sDaemonSetDetailsMetadataConfig,
|
||||
k8sDaemonSetGetEntityName,
|
||||
k8sDaemonSetGetSelectedItemFilters,
|
||||
k8sDaemonSetInitialEventsFilter,
|
||||
k8sDaemonSetInitialFilters,
|
||||
k8sDaemonSetInitialLogTracesFilter,
|
||||
} from './constants';
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
K8sCategory,
|
||||
} from '../constants';
|
||||
import {
|
||||
k8sDaemonSetsColumns,
|
||||
k8sDaemonSetsColumnsConfig,
|
||||
k8sDaemonSetsRenderRowData,
|
||||
} from './table';
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringDaemonSetUID,
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
import K8sHeader from '../K8sHeader';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
import { usePageSize } from '../utils';
|
||||
import DaemonSetDetails from './DaemonSetDetails';
|
||||
import {
|
||||
defaultAddedColumns,
|
||||
formatDataForTable,
|
||||
getK8sDaemonSetsListColumns,
|
||||
getK8sDaemonSetsListQuery,
|
||||
K8sDaemonSetsRowData,
|
||||
} from './utils';
|
||||
|
||||
import '../InfraMonitoringK8s.styles.scss';
|
||||
import './K8sDaemonSetsList.styles.scss';
|
||||
|
||||
function K8sDaemonSetsList({
|
||||
controlListPrefix,
|
||||
isFiltersVisible,
|
||||
handleFilterVisibilityChange,
|
||||
quickFiltersLastUpdated,
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
isFiltersVisible: boolean;
|
||||
handleFilterVisibilityChange: () => void;
|
||||
quickFiltersLastUpdated: number;
|
||||
}): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [filtersInitialised, setFiltersInitialised] = useState(false);
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
|
||||
const [
|
||||
selectedDaemonSetUID,
|
||||
setSelectedDaemonSetUID,
|
||||
] = useInfraMonitoringDaemonSetUID();
|
||||
|
||||
const { pageSize, setPageSize } = usePageSize(K8sCategory.DAEMONSETS);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
|
||||
const [, setView] = useInfraMonitoringView();
|
||||
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
|
||||
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
|
||||
const [, setLogFilters] = useInfraMonitoringLogFilters();
|
||||
|
||||
const [
|
||||
selectedRowData,
|
||||
setSelectedRowData,
|
||||
] = useState<K8sDaemonSetsRowData | null>(null);
|
||||
|
||||
const [groupByOptions, setGroupByOptions] = useState<
|
||||
{ value: string; label: string }[]
|
||||
>([]);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const queryFilters = useMemo(
|
||||
() =>
|
||||
currentQuery?.builder?.queryData[0]?.filters || {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
[currentQuery?.builder?.queryData],
|
||||
);
|
||||
|
||||
// Reset pagination every time quick filters are changed
|
||||
useEffect(() => {
|
||||
if (quickFiltersLastUpdated !== -1) {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
}, [quickFiltersLastUpdated, setCurrentPage]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
filters.orderBy ||= {
|
||||
columnName: 'cpu',
|
||||
order: 'desc',
|
||||
};
|
||||
const createFiltersForSelectedRowData = (
|
||||
selectedRowData: K8sDaemonSetsRowData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): IBuilderQuery['filters'] => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...queryFilters.items],
|
||||
op: 'and',
|
||||
};
|
||||
|
||||
const response = await getK8sDaemonSetsList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
if (!selectedRowData) {
|
||||
return baseFilters;
|
||||
}
|
||||
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
};
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
const { groupedByMeta } = selectedRowData;
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: K8sDaemonSetsData | null; error?: string | null }> => {
|
||||
const response = await getK8sDaemonSetsList(
|
||||
{
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
for (const key of groupBy) {
|
||||
baseFilters.items.push({
|
||||
key: {
|
||||
key: key.key,
|
||||
type: null,
|
||||
},
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
op: '=',
|
||||
value: groupedByMeta[key.key],
|
||||
id: key.key,
|
||||
});
|
||||
}
|
||||
|
||||
const records = response.payload?.data.records || [];
|
||||
return baseFilters;
|
||||
};
|
||||
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
const fetchGroupedByRowDataQuery = useMemo(() => {
|
||||
if (!selectedRowData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseQuery = getK8sDaemonSetsListQuery();
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||
|
||||
return {
|
||||
...baseQuery,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
filters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(() => {
|
||||
// be careful with what you serialize from selectedRowData
|
||||
// since it's react node, it could contain circular references
|
||||
const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta);
|
||||
if (selectedDaemonSetUID) {
|
||||
return [
|
||||
'daemonSetList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'daemonSetList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
queryFilters,
|
||||
orderBy,
|
||||
selectedDaemonSetUID,
|
||||
minTime,
|
||||
maxTime,
|
||||
selectedRowData,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
isLoading: isLoadingGroupedByRowData,
|
||||
isError: isErrorGroupedByRowData,
|
||||
refetch: fetchGroupedByRowData,
|
||||
} = useGetK8sDaemonSetsList(
|
||||
fetchGroupedByRowDataQuery as K8sDaemonSetsListPayload,
|
||||
{
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: GetK8sEntityToAggregateAttribute(
|
||||
K8sCategory.DAEMONSETS,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true,
|
||||
K8sCategory.DAEMONSETS,
|
||||
);
|
||||
|
||||
const query = useMemo(() => {
|
||||
const baseQuery = getK8sDaemonSetsListQuery();
|
||||
const queryPayload = {
|
||||
...baseQuery,
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters: queryFilters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
if (groupBy.length > 0) {
|
||||
queryPayload.groupBy = groupBy;
|
||||
}
|
||||
return queryPayload;
|
||||
}, [pageSize, currentPage, queryFilters, minTime, maxTime, orderBy, groupBy]);
|
||||
|
||||
const formattedGroupedByDaemonSetsData = useMemo(
|
||||
() =>
|
||||
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedDaemonSetUID) {
|
||||
return [
|
||||
'daemonSetList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'daemonSetList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedDaemonSetUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sDaemonSetsList(
|
||||
query as K8sDaemonSetsListPayload,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const daemonSetsData = useMemo(() => data?.payload?.data?.records || [], [
|
||||
data,
|
||||
]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const formattedDaemonSetsData = useMemo(
|
||||
() => formatDataForTable(daemonSetsData, groupBy),
|
||||
[daemonSetsData, groupBy],
|
||||
);
|
||||
|
||||
const nestedDaemonSetsData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) {
|
||||
return [];
|
||||
}
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const columns = useMemo(() => getK8sDaemonSetsListColumns(groupBy), [groupBy]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sDaemonSetsRowData): void => {
|
||||
setSelectedRowData(record);
|
||||
|
||||
if (expandedRowKeys.includes(record.key)) {
|
||||
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
|
||||
} else {
|
||||
setExpandedRowKeys([record.key]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRowData) {
|
||||
fetchGroupedByRowData();
|
||||
}
|
||||
}, [selectedRowData, fetchGroupedByRowData]);
|
||||
|
||||
const handleTableChange: TableProps<K8sDaemonSetsRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter:
|
||||
| SorterResult<K8sDaemonSetsRowData>
|
||||
| SorterResult<K8sDaemonSetsRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
});
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
[setCurrentPage, setOrderBy],
|
||||
);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const handleFiltersChange = useCallback(
|
||||
(value: IBuilderQuery['filters']): void => {
|
||||
handleChangeQueryData('filters', value);
|
||||
if (filtersInitialised) {
|
||||
setCurrentPage(1);
|
||||
} else {
|
||||
setFiltersInitialised(true);
|
||||
}
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
total: data?.payload?.data?.total,
|
||||
});
|
||||
}, [data?.payload?.data?.total]);
|
||||
|
||||
const selectedDaemonSetData = useMemo(() => {
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the daemonset from the formatted grouped by data
|
||||
return (
|
||||
nestedDaemonSetsData.find(
|
||||
(daemonSet) => daemonSet.daemonSetName === selectedDaemonSetUID,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
// If not grouped by, return the daemonset from the daemonsets data
|
||||
return (
|
||||
daemonSetsData.find(
|
||||
(daemonSet) => daemonSet.daemonSetName === selectedDaemonSetUID,
|
||||
) || null
|
||||
);
|
||||
}, [
|
||||
selectedDaemonSetUID,
|
||||
groupBy.length,
|
||||
daemonSetsData,
|
||||
nestedDaemonSetsData,
|
||||
]);
|
||||
|
||||
const openDaemonSetInNewTab = (record: K8sDaemonSetsRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
|
||||
record.daemonsetUID,
|
||||
);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sDaemonSetsRowData,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openDaemonSetInNewTab(record);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
});
|
||||
};
|
||||
|
||||
const nestedColumns = useMemo(() => getK8sDaemonSetsListColumns([]), []);
|
||||
|
||||
const isGroupedByAttribute = groupBy.length > 0;
|
||||
|
||||
const handleExpandedRowViewAllClick = (): void => {
|
||||
if (!selectedRowData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||
|
||||
handleFiltersChange(filters);
|
||||
|
||||
setCurrentPage(1);
|
||||
setSelectedRowData(null);
|
||||
setGroupBy([]);
|
||||
setOrderBy(null);
|
||||
};
|
||||
|
||||
const expandedRowRender = (): JSX.Element => (
|
||||
<div className="expanded-table-container">
|
||||
{isErrorGroupedByRowData && (
|
||||
<Typography>{groupedByRowData?.error || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
{isFetchingGroupedByRowData || isLoadingGroupedByRowData ? (
|
||||
<LoadingContainer />
|
||||
) : (
|
||||
<div className="expanded-table">
|
||||
<Table
|
||||
columns={nestedColumns as ColumnType<K8sDaemonSetsRowData>[]}
|
||||
dataSource={formattedGroupedByDaemonSetsData}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
size="small"
|
||||
loading={{
|
||||
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openDaemonSetInNewTab(record);
|
||||
return;
|
||||
}
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{groupedByRowData?.payload?.data?.total &&
|
||||
groupedByRowData?.payload?.data?.total > 10 ? (
|
||||
<div className="expanded-table-footer">
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
className="periscope-btn secondary"
|
||||
onClick={handleExpandedRowViewAllClick}
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const expandRowIconRenderer = ({
|
||||
expanded,
|
||||
onExpand,
|
||||
record,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
onExpand: (
|
||||
record: K8sDaemonSetsRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sDaemonSetsRowData;
|
||||
}): JSX.Element | null => {
|
||||
if (!isGroupedByAttribute) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return expanded ? (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronDown size={14} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const handleCloseDaemonSetDetail = (): void => {
|
||||
setSelectedDaemonSetUID(null);
|
||||
setView(null);
|
||||
setTracesFilters(null);
|
||||
setEventsFilters(null);
|
||||
setLogFilters(null);
|
||||
};
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
const groupBy = [];
|
||||
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const element = (value[index] as unknown) as string;
|
||||
|
||||
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||
(key) => key.key === element,
|
||||
);
|
||||
|
||||
if (key) {
|
||||
groupBy.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentPage(1);
|
||||
setGroupBy(groupBy);
|
||||
setExpandedRowKeys([]);
|
||||
|
||||
logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
});
|
||||
},
|
||||
[groupByFiltersData, setCurrentPage, setGroupBy],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupByFiltersData?.payload) {
|
||||
setGroupByOptions(
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
);
|
||||
}
|
||||
}, [groupByFiltersData]);
|
||||
|
||||
const onPaginationChange = (page: number, pageSize: number): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedDaemonSetsData.length === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<K8sDaemonSetsData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
isFiltersVisible={isFiltersVisible}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
defaultAddedColumns={defaultAddedColumns}
|
||||
handleFiltersChange={handleFiltersChange}
|
||||
groupByOptions={groupByOptions}
|
||||
isLoadingGroupByFilters={isLoadingGroupByFilters}
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
selectedGroupBy={groupBy}
|
||||
entity={K8sCategory.DAEMONSETS}
|
||||
tableColumnsDefinitions={k8sDaemonSetsColumns}
|
||||
tableColumns={k8sDaemonSetsColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sDaemonSetsRenderRowData}
|
||||
eventCategory={InfraMonitoringEvents.DaemonSet}
|
||||
showAutoRefresh={!selectedDaemonSetData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
<Table
|
||||
className={classNames('k8s-list-table', 'daemonSets-list-table', {
|
||||
'expanded-daemonsets-list-table': isGroupedByAttribute,
|
||||
})}
|
||||
dataSource={showTableLoadingState ? [] : formattedDaemonSetsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: onPaginationChange,
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: showTableLoadingState,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
locale={{
|
||||
emptyText: showTableLoadingState ? null : (
|
||||
<div className="no-filtered-hosts-message-container">
|
||||
<div className="no-filtered-hosts-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-filtered-hosts-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
expandIcon: expandRowIconRenderer,
|
||||
expandedRowKeys,
|
||||
}}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<K8sDaemonSetsData>
|
||||
category={K8sCategory.DAEMONSETS}
|
||||
eventCategory={InfraMonitoringEvents.DaemonSet}
|
||||
getSelectedItemFilters={k8sDaemonSetGetSelectedItemFilters}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sDaemonSetGetEntityName}
|
||||
getInitialLogTracesFilters={k8sDaemonSetInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sDaemonSetInitialEventsFilter}
|
||||
primaryFilterKeys={k8sDaemonSetInitialFilters}
|
||||
metadataConfig={k8sDaemonSetDetailsMetadataConfig}
|
||||
entityWidgetInfo={daemonSetWidgetInfo}
|
||||
getEntityQueryPayload={getDaemonSetMetricsQueryPayload}
|
||||
queryKeyPrefix="daemonset"
|
||||
<DaemonSetDetails
|
||||
daemonSet={selectedDaemonSetData}
|
||||
isModalTimeSelection
|
||||
onClose={handleCloseDaemonSetDetail}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.entityGroupHeader {
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
@@ -1,297 +0,0 @@
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/K8sBaseList';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
formatBytes,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../commonUtils';
|
||||
import { K8sCategory } from '../constants';
|
||||
import { K8sDaemonSetsData } from './api';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export const k8sDaemonSetsColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'DaemonSet Group',
|
||||
value: 'daemonSetGroup',
|
||||
id: 'daemonSetGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'DaemonSet Name',
|
||||
value: 'daemonsetName',
|
||||
id: 'daemonsetName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Available',
|
||||
value: 'available_nodes',
|
||||
id: 'available_nodes',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Desired',
|
||||
value: 'desired_nodes',
|
||||
id: 'desired_nodes',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Req Usage (%)',
|
||||
value: 'cpu_request',
|
||||
id: 'cpu_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Limit Usage (%)',
|
||||
value: 'cpu_limit',
|
||||
id: 'cpu_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Req Usage (%)',
|
||||
value: 'memory_request',
|
||||
id: 'memory_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Limit Usage (%)',
|
||||
value: 'memory_limit',
|
||||
id: 'memory_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDaemonSetsColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> DAEMONSET GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'daemonSetGroup',
|
||||
key: 'daemonSetGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
},
|
||||
{
|
||||
title: <div>DaemonSet Name</div>,
|
||||
dataIndex: 'daemonsetName',
|
||||
key: 'daemonsetName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Namespace Name</div>,
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Available</div>,
|
||||
dataIndex: 'available_nodes',
|
||||
key: 'available_nodes',
|
||||
width: 50,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Desired</div>,
|
||||
dataIndex: 'desired_nodes',
|
||||
key: 'desired_nodes',
|
||||
width: 50,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Req Usage (%)</div>,
|
||||
dataIndex: 'cpu_request',
|
||||
key: 'cpu_request',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Limit Usage (%)</div>,
|
||||
dataIndex: 'cpu_limit',
|
||||
key: 'cpu_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Req Usage (%)</div>,
|
||||
dataIndex: 'memory_request',
|
||||
key: 'memory_request',
|
||||
width: 170,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Limit Usage (%)</div>,
|
||||
dataIndex: 'memory_limit',
|
||||
key: 'memory_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDaemonSetsRenderRowData = (
|
||||
entity: K8sDaemonSetsData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(
|
||||
entity,
|
||||
() => entity.daemonSetName || entity.meta.k8s_daemonset_name || '',
|
||||
groupBy,
|
||||
),
|
||||
itemKey: entity.meta.k8s_daemonset_name,
|
||||
daemonsetName: (
|
||||
<Tooltip title={entity.meta.k8s_daemonset_name}>
|
||||
{entity.meta.k8s_daemonset_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
namespaceName: (
|
||||
<Tooltip title={entity.meta.k8s_namespace_name}>
|
||||
{entity.meta.k8s_namespace_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={entity.cpuRequest}
|
||||
entity={K8sCategory.DAEMONSETS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={entity.cpuRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={entity.cpuLimit}
|
||||
entity={K8sCategory.DAEMONSETS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={entity.cpuLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={entity.cpuUsage}>
|
||||
{entity.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={entity.memoryRequest}
|
||||
entity={K8sCategory.DAEMONSETS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={entity.memoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={entity.memoryLimit}
|
||||
entity={K8sCategory.DAEMONSETS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={entity.memoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={entity.memoryUsage}>
|
||||
{formatBytes(entity.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
available_nodes: (
|
||||
<ValidateColumnValueWrapper value={entity.availableNodes}>
|
||||
{entity.availableNodes}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
desired_nodes: (
|
||||
<ValidateColumnValueWrapper value={entity.desiredNodes}>
|
||||
{entity.desiredNodes}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
daemonSetGroup: getGroupByEl(entity, groupBy),
|
||||
...entity.meta,
|
||||
groupedByMeta: getGroupedByMeta(entity, groupBy),
|
||||
});
|
||||
356
frontend/src/container/InfraMonitoringK8s/DaemonSets/utils.tsx
Normal file
356
frontend/src/container/InfraMonitoringK8s/DaemonSets/utils.tsx
Normal file
@@ -0,0 +1,356 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
|
||||
import {
|
||||
K8sDaemonSetsData,
|
||||
K8sDaemonSetsListPayload,
|
||||
} from 'api/infraMonitoring/getK8sDaemonSetsList';
|
||||
import { Group } from 'lucide-react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
EntityProgressBar,
|
||||
formatBytes,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../commonUtils';
|
||||
import { K8sCategory } from '../constants';
|
||||
import { IEntityColumn } from '../utils';
|
||||
|
||||
export const defaultAddedColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'DaemonSet Name',
|
||||
value: 'daemonsetName',
|
||||
id: 'daemonsetName',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Available',
|
||||
value: 'available_nodes',
|
||||
id: 'available_nodes',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Desired',
|
||||
value: 'desired_nodes',
|
||||
id: 'desired_nodes',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Req Usage (%)',
|
||||
value: 'cpu_request',
|
||||
id: 'cpu_request',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Limit Usage (%)',
|
||||
value: 'cpu_limit',
|
||||
id: 'cpu_limit',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Mem Req Usage (%)',
|
||||
value: 'memory_request',
|
||||
id: 'memory_request',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Mem Limit Usage (%)',
|
||||
value: 'memory_limit',
|
||||
id: 'memory_limit',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Mem Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canRemove: false,
|
||||
},
|
||||
];
|
||||
|
||||
export interface K8sDaemonSetsRowData {
|
||||
key: string;
|
||||
daemonsetUID: string;
|
||||
daemonsetName: React.ReactNode;
|
||||
cpu_request: React.ReactNode;
|
||||
cpu_limit: React.ReactNode;
|
||||
cpu: React.ReactNode;
|
||||
memory_request: React.ReactNode;
|
||||
memory_limit: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
desired_nodes: React.ReactNode;
|
||||
available_nodes: React.ReactNode;
|
||||
namespaceName: React.ReactNode;
|
||||
groupedByMeta?: any;
|
||||
}
|
||||
|
||||
const daemonSetGroupColumnConfig = {
|
||||
title: (
|
||||
<div className="column-header entity-group-header">
|
||||
<Group size={14} /> DAEMONSET GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'daemonSetGroup',
|
||||
key: 'daemonSetGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
className: 'column entity-group-header',
|
||||
};
|
||||
|
||||
export const getK8sDaemonSetsListQuery = (): K8sDaemonSetsListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
const columnProgressBarClassName = 'column-progress-bar';
|
||||
|
||||
const columnsConfig = [
|
||||
{
|
||||
title: (
|
||||
<div className="column-header-left daemonset-name-header">
|
||||
DaemonSet Name
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'daemonsetName',
|
||||
key: 'daemonsetName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className="column-header-left namespace-name-header">
|
||||
Namespace Name
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header small-col">Available</div>,
|
||||
dataIndex: 'available_nodes',
|
||||
key: 'available_nodes',
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header small-col">Desired</div>,
|
||||
dataIndex: 'desired_nodes',
|
||||
key: 'desired_nodes',
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header med-col">CPU Req Usage (%)</div>,
|
||||
dataIndex: 'cpu_request',
|
||||
key: 'cpu_request',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header med-col">CPU Limit Usage (%)</div>,
|
||||
dataIndex: 'cpu_limit',
|
||||
key: 'cpu_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header small-col">CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header med-col">Mem Req Usage (%)</div>,
|
||||
dataIndex: 'memory_request',
|
||||
key: 'memory_request',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header med-col">Mem Limit Usage (%)</div>,
|
||||
dataIndex: 'memory_limit',
|
||||
key: 'memory_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header med-col">Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
];
|
||||
|
||||
export const getK8sDaemonSetsListColumns = (
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): ColumnType<K8sDaemonSetsRowData>[] => {
|
||||
if (groupBy.length > 0) {
|
||||
const filteredColumns = [...columnsConfig].filter(
|
||||
(column) => column.key !== 'daemonsetName',
|
||||
);
|
||||
filteredColumns.unshift(daemonSetGroupColumnConfig);
|
||||
return filteredColumns as ColumnType<K8sDaemonSetsRowData>[];
|
||||
}
|
||||
|
||||
return columnsConfig as ColumnType<K8sDaemonSetsRowData>[];
|
||||
};
|
||||
|
||||
const dotToUnder: Record<string, keyof K8sDaemonSetsData['meta']> = {
|
||||
'k8s.daemonset.name': 'k8s_daemonset_name',
|
||||
'k8s.namespace.name': 'k8s_namespace_name',
|
||||
'k8s.cluster.name': 'k8s_cluster_name',
|
||||
};
|
||||
|
||||
const getGroupByEle = (
|
||||
daemonSet: K8sDaemonSetsData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): React.ReactNode => {
|
||||
const groupByValues: string[] = [];
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
|
||||
// Choose mapped key if present, otherwise use rawKey
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof daemonSet.meta;
|
||||
const value = daemonSet.meta[metaKey];
|
||||
|
||||
groupByValues.push(value);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="pod-group">
|
||||
{groupByValues.map((value) => (
|
||||
<Tag key={value} color={Color.BG_SLATE_400} className="pod-group-tag-item">
|
||||
{value === '' ? '<no-value>' : value}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const formatDataForTable = (
|
||||
data: K8sDaemonSetsData[],
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): K8sDaemonSetsRowData[] =>
|
||||
data.map((daemonSet, index) => ({
|
||||
key: index.toString(),
|
||||
daemonsetUID: daemonSet.daemonSetName,
|
||||
daemonsetName: (
|
||||
<Tooltip title={daemonSet.meta.k8s_daemonset_name}>
|
||||
{daemonSet.meta.k8s_daemonset_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
namespaceName: (
|
||||
<Tooltip title={daemonSet.meta.k8s_namespace_name}>
|
||||
{daemonSet.meta.k8s_namespace_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={daemonSet.cpuRequest}
|
||||
entity={K8sCategory.DAEMONSETS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={daemonSet.cpuRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={daemonSet.cpuLimit}
|
||||
entity={K8sCategory.DAEMONSETS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={daemonSet.cpuLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={daemonSet.cpuUsage}>
|
||||
{daemonSet.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={daemonSet.memoryRequest}
|
||||
entity={K8sCategory.DAEMONSETS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={daemonSet.memoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={daemonSet.memoryLimit}
|
||||
entity={K8sCategory.DAEMONSETS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={daemonSet.memoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={daemonSet.memoryUsage}>
|
||||
{formatBytes(daemonSet.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
available_nodes: (
|
||||
<ValidateColumnValueWrapper value={daemonSet.availableNodes}>
|
||||
{daemonSet.availableNodes}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
desired_nodes: (
|
||||
<ValidateColumnValueWrapper value={daemonSet.desiredNodes}>
|
||||
{daemonSet.desiredNodes}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
daemonSetGroup: getGroupByEle(daemonSet, groupBy),
|
||||
meta: daemonSet.meta,
|
||||
...daemonSet.meta,
|
||||
groupedByMeta: daemonSet.meta,
|
||||
}));
|
||||
@@ -0,0 +1,7 @@
|
||||
import { K8sDeploymentsData } from 'api/infraMonitoring/getK8sDeploymentsList';
|
||||
|
||||
export type DeploymentDetailsProps = {
|
||||
deployment: K8sDeploymentsData | null;
|
||||
isModalTimeSelection: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -0,0 +1,671 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||
import type { RadioChangeEvent } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { K8sDeploymentsData } from 'api/infraMonitoring/getK8sDeploymentsList';
|
||||
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
|
||||
import {
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import {
|
||||
BarChart2,
|
||||
ChevronsLeftRight,
|
||||
Compass,
|
||||
DraftingCompass,
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import DeploymentEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
import DeploymentLogs from '../../EntityDetailsUtils/EntityLogs';
|
||||
import DeploymentMetrics from '../../EntityDetailsUtils/EntityMetrics';
|
||||
import DeploymentTraces from '../../EntityDetailsUtils/EntityTraces';
|
||||
import {
|
||||
deploymentWidgetInfo,
|
||||
getDeploymentMetricsQueryPayload,
|
||||
} from './constants';
|
||||
import { DeploymentDetailsProps } from './DeploymentDetails.interfaces';
|
||||
|
||||
import '../../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
|
||||
function DeploymentDetails({
|
||||
deployment,
|
||||
onClose,
|
||||
isModalTimeSelection,
|
||||
}: DeploymentDetailsProps): JSX.Element {
|
||||
const { maxTime, minTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
|
||||
minTime,
|
||||
]);
|
||||
const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
||||
startTime: startMs,
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [
|
||||
tracesFiltersParam,
|
||||
setTracesFiltersParam,
|
||||
] = useInfraMonitoringTracesFilters();
|
||||
const [
|
||||
eventsFiltersParam,
|
||||
setEventsFiltersParam,
|
||||
] = useInfraMonitoringEventsFilters();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const initialFilters = useMemo(() => {
|
||||
const filters =
|
||||
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
|
||||
if (filters) {
|
||||
return filters;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_deployment_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: deployment?.meta.k8s_deployment_name || '',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_deployment_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: deployment?.meta.k8s_namespace_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [
|
||||
deployment?.meta.k8s_deployment_name,
|
||||
deployment?.meta.k8s_namespace_name,
|
||||
selectedView,
|
||||
logFiltersParam,
|
||||
tracesFiltersParam,
|
||||
]);
|
||||
|
||||
const initialEventsFilters = useMemo(() => {
|
||||
if (eventsFiltersParam) {
|
||||
return eventsFiltersParam;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.kind--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: 'Deployment',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: deployment?.meta.k8s_deployment_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [deployment?.meta.k8s_deployment_name, eventsFiltersParam]);
|
||||
|
||||
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
|
||||
IBuilderQuery['filters']
|
||||
>(initialFilters);
|
||||
|
||||
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
|
||||
initialEventsFilters,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (deployment) {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
});
|
||||
}
|
||||
}, [deployment]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogAndTracesFilters(initialFilters);
|
||||
setEventsFilters(initialEventsFilters);
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
}, [selectedTime, minTime, maxTime]);
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
setLogFiltersParam(null);
|
||||
setTracesFiltersParam(null);
|
||||
setEventsFiltersParam(null);
|
||||
logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
view: e.target.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 / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
interval,
|
||||
view: selectedView,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeLogFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||
item.key?.key ?? '',
|
||||
),
|
||||
);
|
||||
const paginationFilter = value?.items?.find(
|
||||
(item) => item.key?.key === 'id',
|
||||
);
|
||||
const newFilters = value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== 'id' &&
|
||||
item.key?.key !== QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
view: InfraMonitoringEvents.LogsView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(newFilters || []),
|
||||
...(paginationFilter ? [paginationFilter] : []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setLogFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeTracesFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||
item.key?.key ?? '',
|
||||
),
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(value?.items?.filter(
|
||||
(item) => item.key?.key !== QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setTracesFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeEventsFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setEventsFilters((prevFilters) => {
|
||||
const deploymentKindFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
);
|
||||
const deploymentNameFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
view: InfraMonitoringEvents.EventsView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
deploymentKindFilter,
|
||||
deploymentNameFilter,
|
||||
...(value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setEventsFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleExplorePagesRedirect = (): void => {
|
||||
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: InfraMonitoringEvents.Deployment,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const filtersWithoutPagination = {
|
||||
...logAndTracesFilters,
|
||||
items: logAndTracesFilters?.items?.filter((item) => item.key?.key !== 'id'),
|
||||
};
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.logs,
|
||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||
filters: filtersWithoutPagination,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
aggregateOperator: TracesAggregatorOperator.NOOP,
|
||||
filters: logAndTracesFilters,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
setSelectedView(VIEW_TYPES.METRICS);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="70%"
|
||||
title={
|
||||
<>
|
||||
<Divider type="vertical" />
|
||||
<Typography.Text className="title">
|
||||
{deployment?.meta.k8s_deployment_name}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!deployment}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="entity-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{deployment && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Deployment Name
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Cluster Name
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Namespace Name
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="values-row">
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={deployment.meta.k8s_deployment_name}>
|
||||
{deployment.meta.k8s_deployment_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={deployment.meta.k8s_cluster_name}>
|
||||
{deployment.meta.k8s_cluster_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={deployment.meta.k8s_namespace_name}>
|
||||
{deployment.meta.k8s_namespace_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="views-tabs-container">
|
||||
<Radio.Group
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.METRICS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<BarChart2 size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.LOGS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.LOGS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.TRACES ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.TRACES}
|
||||
>
|
||||
<div className="view-title">
|
||||
<DraftingCompass size={14} />
|
||||
Traces
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.EVENTS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.EVENTS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Events
|
||||
</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
|
||||
{(selectedView === VIEW_TYPES.LOGS ||
|
||||
selectedView === VIEW_TYPES.TRACES) && (
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{selectedView === VIEW_TYPES.METRICS && (
|
||||
<DeploymentMetrics<K8sDeploymentsData>
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
entity={deployment}
|
||||
entityWidgetInfo={deploymentWidgetInfo}
|
||||
getEntityQueryPayload={getDeploymentMetricsQueryPayload}
|
||||
category={K8sCategory.DEPLOYMENTS}
|
||||
queryKey="deploymentMetrics"
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<DeploymentLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKeyFilters={[
|
||||
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
]}
|
||||
queryKey="deploymentLogs"
|
||||
category={K8sCategory.DEPLOYMENTS}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<DeploymentTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||
tracesFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="deploymentTraces"
|
||||
category={InfraMonitoringEvents.Deployment}
|
||||
queryKeyFilters={[
|
||||
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.EVENTS && (
|
||||
<DeploymentEvents
|
||||
timeRange={modalTimeRange}
|
||||
handleChangeEventFilters={handleChangeEventsFilters}
|
||||
filters={eventsFilters}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
category={K8sCategory.DEPLOYMENTS}
|
||||
queryKey="deploymentEvents"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeploymentDetails;
|
||||
@@ -1,75 +1,11 @@
|
||||
import { K8sDeploymentsData } from 'api/infraMonitoring/getK8sDeploymentsList';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
createFilterItem,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from '../Base/K8sBaseDetails';
|
||||
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
|
||||
import { K8sDeploymentsData } from './api';
|
||||
|
||||
export const k8sDeploymentGetSelectedItemFilters = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_deployment_name',
|
||||
key: {
|
||||
key: 'k8s_deployment_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const k8sDeploymentDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sDeploymentsData>[] = [
|
||||
{
|
||||
label: 'Deployment Name',
|
||||
getValue: (p): string => p.meta.k8s_deployment_name,
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
getValue: (p): string => p.meta.k8s_cluster_name,
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
getValue: (p): string => p.meta.k8s_namespace_name,
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDeploymentInitialFilters = [
|
||||
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
];
|
||||
|
||||
export const k8sDeploymentInitialEventsFilter = (
|
||||
item: K8sDeploymentsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Deployment'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_deployment_name),
|
||||
];
|
||||
|
||||
export const k8sDeploymentInitialLogTracesFilter = (
|
||||
item: K8sDeploymentsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(
|
||||
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
item.meta.k8s_deployment_name,
|
||||
),
|
||||
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
|
||||
];
|
||||
|
||||
export const k8sDeploymentGetEntityName = (item: K8sDeploymentsData): string =>
|
||||
item.meta.k8s_deployment_name;
|
||||
|
||||
export const deploymentWidgetInfo = [
|
||||
{
|
||||
title: 'CPU usage, request, limits',
|
||||
@@ -0,0 +1,3 @@
|
||||
import DeploymentDetails from './DeploymentDetails';
|
||||
|
||||
export default DeploymentDetails;
|
||||
@@ -0,0 +1,57 @@
|
||||
.infra-monitoring-container {
|
||||
.deployments-list-table {
|
||||
.expanded-table-container {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.deployment-name-header) {
|
||||
min-width: 250px !important;
|
||||
max-width: 250px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.namespace-name-header) {
|
||||
min-width: 220px !important;
|
||||
max-width: 220px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.med-col) {
|
||||
min-width: 200px !important;
|
||||
max-width: 200px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.small-col) {
|
||||
min-width: 120px !important;
|
||||
max-width: 120px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex !important;
|
||||
justify-content: start !important;
|
||||
}
|
||||
|
||||
.expanded-deployments-list-table {
|
||||
.ant-table-cell {
|
||||
min-width: 180px !important;
|
||||
max-width: 180px !important;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
}
|
||||
@@ -1,116 +1,722 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Spin,
|
||||
Table,
|
||||
TableColumnType as ColumnType,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { K8sDeploymentsListPayload } from 'api/infraMonitoring/getK8sDeploymentsList';
|
||||
import classNames from 'classnames';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useGetK8sDeploymentsList } from 'hooks/infraMonitoring/useGetK8sDeploymentsList';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseFilters, K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sCategory } from '../constants';
|
||||
import { getK8sDeploymentsList, K8sDeploymentsData } from './api';
|
||||
import {
|
||||
deploymentWidgetInfo,
|
||||
getDeploymentMetricsQueryPayload,
|
||||
k8sDeploymentDetailsMetadataConfig,
|
||||
k8sDeploymentGetEntityName,
|
||||
k8sDeploymentGetSelectedItemFilters,
|
||||
k8sDeploymentInitialEventsFilter,
|
||||
k8sDeploymentInitialFilters,
|
||||
k8sDeploymentInitialLogTracesFilter,
|
||||
} from './constants';
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
K8sCategory,
|
||||
} from '../constants';
|
||||
import {
|
||||
k8sDeploymentsColumns,
|
||||
k8sDeploymentsColumnsConfig,
|
||||
k8sDeploymentsRenderRowData,
|
||||
} from './table';
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringDeploymentUID,
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
import K8sHeader from '../K8sHeader';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
import { usePageSize } from '../utils';
|
||||
import DeploymentDetails from './DeploymentDetails';
|
||||
import {
|
||||
defaultAddedColumns,
|
||||
formatDataForTable,
|
||||
getK8sDeploymentsListColumns,
|
||||
getK8sDeploymentsListQuery,
|
||||
K8sDeploymentsRowData,
|
||||
} from './utils';
|
||||
|
||||
import '../InfraMonitoringK8s.styles.scss';
|
||||
import './K8sDeploymentsList.styles.scss';
|
||||
|
||||
function K8sDeploymentsList({
|
||||
controlListPrefix,
|
||||
isFiltersVisible,
|
||||
handleFilterVisibilityChange,
|
||||
quickFiltersLastUpdated,
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
isFiltersVisible: boolean;
|
||||
handleFilterVisibilityChange: () => void;
|
||||
quickFiltersLastUpdated: number;
|
||||
}): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [filtersInitialised, setFiltersInitialised] = useState(false);
|
||||
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
|
||||
const [
|
||||
selectedDeploymentUID,
|
||||
setselectedDeploymentUID,
|
||||
] = useInfraMonitoringDeploymentUID();
|
||||
|
||||
const { pageSize, setPageSize } = usePageSize(K8sCategory.DEPLOYMENTS);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
|
||||
const [, setView] = useInfraMonitoringView();
|
||||
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
|
||||
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
|
||||
const [, setLogFilters] = useInfraMonitoringLogFilters();
|
||||
|
||||
const [
|
||||
selectedRowData,
|
||||
setSelectedRowData,
|
||||
] = useState<K8sDeploymentsRowData | null>(null);
|
||||
|
||||
const [groupByOptions, setGroupByOptions] = useState<
|
||||
{ value: string; label: string }[]
|
||||
>([]);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const queryFilters = useMemo(
|
||||
() =>
|
||||
currentQuery?.builder?.queryData[0]?.filters || {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
[currentQuery?.builder?.queryData],
|
||||
);
|
||||
|
||||
// Reset pagination every time quick filters are changed
|
||||
useEffect(() => {
|
||||
if (quickFiltersLastUpdated !== -1) {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
}, [quickFiltersLastUpdated, setCurrentPage]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
filters.orderBy ||= {
|
||||
columnName: 'cpu',
|
||||
order: 'desc',
|
||||
};
|
||||
const createFiltersForSelectedRowData = (
|
||||
selectedRowData: K8sDeploymentsRowData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): IBuilderQuery['filters'] => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...queryFilters.items],
|
||||
op: 'and',
|
||||
};
|
||||
|
||||
const response = await getK8sDeploymentsList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
if (!selectedRowData) {
|
||||
return baseFilters;
|
||||
}
|
||||
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
};
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
const { groupedByMeta } = selectedRowData;
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: K8sDeploymentsData | null; error?: string | null }> => {
|
||||
const response = await getK8sDeploymentsList(
|
||||
{
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
for (const key of groupBy) {
|
||||
baseFilters.items.push({
|
||||
key: {
|
||||
key: key.key,
|
||||
type: null,
|
||||
},
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
op: '=',
|
||||
value: groupedByMeta[key.key],
|
||||
id: key.key,
|
||||
});
|
||||
}
|
||||
|
||||
const records = response.payload?.data.records || [];
|
||||
return baseFilters;
|
||||
};
|
||||
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
const fetchGroupedByRowDataQuery = useMemo(() => {
|
||||
if (!selectedRowData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseQuery = getK8sDeploymentsListQuery();
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||
|
||||
return {
|
||||
...baseQuery,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
filters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(() => {
|
||||
// be careful with what you serialize from selectedRowData
|
||||
// since it's react node, it could contain circular references
|
||||
const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta);
|
||||
if (selectedDeploymentUID) {
|
||||
return [
|
||||
'deploymentList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'deploymentList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
queryFilters,
|
||||
orderBy,
|
||||
selectedDeploymentUID,
|
||||
minTime,
|
||||
maxTime,
|
||||
selectedRowData,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
isLoading: isLoadingGroupedByRowData,
|
||||
isError: isErrorGroupedByRowData,
|
||||
refetch: fetchGroupedByRowData,
|
||||
} = useGetK8sDeploymentsList(
|
||||
fetchGroupedByRowDataQuery as K8sDeploymentsListPayload,
|
||||
{
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: GetK8sEntityToAggregateAttribute(
|
||||
K8sCategory.DEPLOYMENTS,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true,
|
||||
K8sCategory.NODES,
|
||||
);
|
||||
|
||||
const query = useMemo(() => {
|
||||
const baseQuery = getK8sDeploymentsListQuery();
|
||||
const queryPayload = {
|
||||
...baseQuery,
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters: queryFilters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
if (groupBy.length > 0) {
|
||||
queryPayload.groupBy = groupBy;
|
||||
}
|
||||
return queryPayload;
|
||||
}, [pageSize, currentPage, queryFilters, minTime, maxTime, orderBy, groupBy]);
|
||||
|
||||
const formattedGroupedByDeploymentsData = useMemo(
|
||||
() =>
|
||||
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedDeploymentUID) {
|
||||
return [
|
||||
'deploymentList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'deploymentList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedDeploymentUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sDeploymentsList(
|
||||
query as K8sDeploymentsListPayload,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const deploymentsData = useMemo(() => data?.payload?.data?.records || [], [
|
||||
data,
|
||||
]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const formattedDeploymentsData = useMemo(
|
||||
() => formatDataForTable(deploymentsData, groupBy),
|
||||
[deploymentsData, groupBy],
|
||||
);
|
||||
|
||||
const nestedDeploymentsData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) {
|
||||
return [];
|
||||
}
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const columns = useMemo(() => getK8sDeploymentsListColumns(groupBy), [
|
||||
groupBy,
|
||||
]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sDeploymentsRowData): void => {
|
||||
setSelectedRowData(record);
|
||||
|
||||
if (expandedRowKeys.includes(record.key)) {
|
||||
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
|
||||
} else {
|
||||
setExpandedRowKeys([record.key]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRowData) {
|
||||
fetchGroupedByRowData();
|
||||
}
|
||||
}, [selectedRowData, fetchGroupedByRowData]);
|
||||
|
||||
const handleTableChange: TableProps<K8sDeploymentsRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter:
|
||||
| SorterResult<K8sDeploymentsRowData>
|
||||
| SorterResult<K8sDeploymentsRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
});
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
[setCurrentPage, setOrderBy],
|
||||
);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const handleFiltersChange = useCallback(
|
||||
(value: IBuilderQuery['filters']): void => {
|
||||
handleChangeQueryData('filters', value);
|
||||
if (filtersInitialised) {
|
||||
setCurrentPage(1);
|
||||
} else {
|
||||
setFiltersInitialised(true);
|
||||
}
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
total: data?.payload?.data?.total,
|
||||
});
|
||||
}, [data?.payload?.data?.total]);
|
||||
|
||||
const selectedDeploymentData = useMemo(() => {
|
||||
if (!selectedDeploymentUID) {
|
||||
return null;
|
||||
}
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the deployment from the formatted grouped by deployments data
|
||||
return (
|
||||
nestedDeploymentsData.find(
|
||||
(deployment) => deployment.deploymentName === selectedDeploymentUID,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
// If not grouped by, return the deployment from the deployments data
|
||||
return (
|
||||
deploymentsData.find(
|
||||
(deployment) => deployment.deploymentName === selectedDeploymentUID,
|
||||
) || null
|
||||
);
|
||||
}, [
|
||||
selectedDeploymentUID,
|
||||
groupBy.length,
|
||||
deploymentsData,
|
||||
nestedDeploymentsData,
|
||||
]);
|
||||
|
||||
const openDeploymentInNewTab = (record: K8sDeploymentsRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
|
||||
record.deploymentUID,
|
||||
);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sDeploymentsRowData,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openDeploymentInNewTab(record);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
});
|
||||
};
|
||||
|
||||
const nestedColumns = useMemo(() => getK8sDeploymentsListColumns([]), []);
|
||||
|
||||
const isGroupedByAttribute = groupBy.length > 0;
|
||||
|
||||
const handleExpandedRowViewAllClick = (): void => {
|
||||
if (!selectedRowData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||
|
||||
handleFiltersChange(filters);
|
||||
|
||||
setCurrentPage(1);
|
||||
setSelectedRowData(null);
|
||||
setGroupBy([]);
|
||||
setOrderBy(null);
|
||||
};
|
||||
|
||||
const expandedRowRender = (): JSX.Element => (
|
||||
<div className="expanded-table-container">
|
||||
{isErrorGroupedByRowData && (
|
||||
<Typography>{groupedByRowData?.error || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
{isFetchingGroupedByRowData || isLoadingGroupedByRowData ? (
|
||||
<LoadingContainer />
|
||||
) : (
|
||||
<div className="expanded-table">
|
||||
<Table
|
||||
columns={nestedColumns as ColumnType<K8sDeploymentsRowData>[]}
|
||||
dataSource={formattedGroupedByDeploymentsData}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
size="small"
|
||||
loading={{
|
||||
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openDeploymentInNewTab(record);
|
||||
return;
|
||||
}
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{groupedByRowData?.payload?.data?.total &&
|
||||
groupedByRowData?.payload?.data?.total > 10 ? (
|
||||
<div className="expanded-table-footer">
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
className="periscope-btn secondary"
|
||||
onClick={handleExpandedRowViewAllClick}
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const expandRowIconRenderer = ({
|
||||
expanded,
|
||||
onExpand,
|
||||
record,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
onExpand: (
|
||||
record: K8sDeploymentsRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sDeploymentsRowData;
|
||||
}): JSX.Element | null => {
|
||||
if (!isGroupedByAttribute) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return expanded ? (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronDown size={14} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const handleCloseDeploymentDetail = (): void => {
|
||||
setselectedDeploymentUID(null);
|
||||
setView(null);
|
||||
setTracesFilters(null);
|
||||
setEventsFilters(null);
|
||||
setLogFilters(null);
|
||||
};
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
const groupBy = [];
|
||||
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const element = (value[index] as unknown) as string;
|
||||
|
||||
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||
(key) => key.key === element,
|
||||
);
|
||||
|
||||
if (key) {
|
||||
groupBy.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset pagination on switching to groupBy
|
||||
setCurrentPage(1);
|
||||
setGroupBy(groupBy);
|
||||
setExpandedRowKeys([]);
|
||||
|
||||
logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
});
|
||||
},
|
||||
[groupByFiltersData, setCurrentPage, setGroupBy],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupByFiltersData?.payload) {
|
||||
setGroupByOptions(
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
);
|
||||
}
|
||||
}, [groupByFiltersData]);
|
||||
|
||||
const onPaginationChange = (page: number, pageSize: number): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedDeploymentsData.length === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<K8sDeploymentsData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={K8sCategory.DEPLOYMENTS}
|
||||
tableColumnsDefinitions={k8sDeploymentsColumns}
|
||||
tableColumns={k8sDeploymentsColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sDeploymentsRenderRowData}
|
||||
eventCategory={InfraMonitoringEvents.Deployment}
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
isFiltersVisible={isFiltersVisible}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
defaultAddedColumns={defaultAddedColumns}
|
||||
handleFiltersChange={handleFiltersChange}
|
||||
groupByOptions={groupByOptions}
|
||||
isLoadingGroupByFilters={isLoadingGroupByFilters}
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
selectedGroupBy={groupBy}
|
||||
entity={K8sCategory.NODES}
|
||||
showAutoRefresh={!selectedDeploymentData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
<Table
|
||||
className={classNames('k8s-list-table', 'deployments-list-table', {
|
||||
'expanded-deployments-list-table': isGroupedByAttribute,
|
||||
})}
|
||||
dataSource={showTableLoadingState ? [] : formattedDeploymentsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: onPaginationChange,
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: showTableLoadingState,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
locale={{
|
||||
emptyText: showTableLoadingState ? null : (
|
||||
<div className="no-filtered-hosts-message-container">
|
||||
<div className="no-filtered-hosts-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-filtered-hosts-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
expandIcon: expandRowIconRenderer,
|
||||
expandedRowKeys,
|
||||
}}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<K8sDeploymentsData>
|
||||
category={K8sCategory.DEPLOYMENTS}
|
||||
eventCategory={InfraMonitoringEvents.Deployment}
|
||||
getSelectedItemFilters={k8sDeploymentGetSelectedItemFilters}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sDeploymentGetEntityName}
|
||||
getInitialLogTracesFilters={k8sDeploymentInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sDeploymentInitialEventsFilter}
|
||||
primaryFilterKeys={k8sDeploymentInitialFilters}
|
||||
metadataConfig={k8sDeploymentDetailsMetadataConfig}
|
||||
entityWidgetInfo={deploymentWidgetInfo}
|
||||
getEntityQueryPayload={getDeploymentMetricsQueryPayload}
|
||||
queryKeyPrefix="deployment"
|
||||
<DeploymentDetails
|
||||
deployment={selectedDeploymentData}
|
||||
isModalTimeSelection
|
||||
onClose={handleCloseDeploymentDetail}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.entityGroupHeader {
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/K8sBaseList';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
formatBytes,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../commonUtils';
|
||||
import { K8sDeploymentsData } from './api';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export const k8sDeploymentsColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Deployment Group',
|
||||
value: 'deploymentGroup',
|
||||
id: 'deploymentGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'Deployment Name',
|
||||
value: 'deploymentName',
|
||||
id: 'deploymentName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Available',
|
||||
value: 'available_pods',
|
||||
id: 'available_pods',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Desired',
|
||||
value: 'desired_pods',
|
||||
id: 'desired_pods',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Req Usage (%)',
|
||||
value: 'cpu_request',
|
||||
id: 'cpu_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Limit Usage (%)',
|
||||
value: 'cpu_limit',
|
||||
id: 'cpu_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Req Usage (%)',
|
||||
value: 'memory_request',
|
||||
id: 'memory_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Limit Usage (%)',
|
||||
value: 'memory_limit',
|
||||
id: 'memory_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDeploymentsColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> DEPLOYMENT GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'deploymentGroup',
|
||||
key: 'deploymentGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
},
|
||||
{
|
||||
title: <div>Deployment Name</div>,
|
||||
dataIndex: 'deploymentName',
|
||||
key: 'deploymentName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Namespace Name</div>,
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Available</div>,
|
||||
dataIndex: 'available_pods',
|
||||
key: 'available_pods',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Desired</div>,
|
||||
dataIndex: 'desired_pods',
|
||||
key: 'desired_pods',
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Req Usage (%)</div>,
|
||||
dataIndex: 'cpu_request',
|
||||
key: 'cpu_request',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Limit Usage (%)</div>,
|
||||
dataIndex: 'cpu_limit',
|
||||
key: 'cpu_limit',
|
||||
width: 50,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Req Usage (%)</div>,
|
||||
dataIndex: 'memory_request',
|
||||
key: 'memory_request',
|
||||
width: 50,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Limit Usage (%)</div>,
|
||||
dataIndex: 'memory_limit',
|
||||
key: 'memory_limit',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDeploymentsRenderRowData = (
|
||||
deployment: K8sDeploymentsData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(deployment, () => deployment.meta.k8s_deployment_name, groupBy),
|
||||
itemKey: deployment.meta.k8s_deployment_name,
|
||||
deploymentName: (
|
||||
<Tooltip title={deployment.meta.k8s_deployment_name}>
|
||||
{deployment.meta.k8s_deployment_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
namespaceName: deployment.meta.k8s_namespace_name,
|
||||
available_pods: (
|
||||
<ValidateColumnValueWrapper value={deployment.availablePods}>
|
||||
{deployment.availablePods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
desired_pods: (
|
||||
<ValidateColumnValueWrapper value={deployment.desiredPods}>
|
||||
{deployment.desiredPods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={deployment.cpuUsage}>
|
||||
{deployment.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper value={deployment.cpuRequest}>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={deployment.cpuRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper value={deployment.cpuLimit}>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={deployment.cpuLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={deployment.memoryUsage}>
|
||||
{formatBytes(deployment.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper value={deployment.memoryRequest}>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={deployment.memoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper value={deployment.memoryLimit}>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={deployment.memoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
deploymentGroup: getGroupByEl(deployment, groupBy),
|
||||
...deployment.meta,
|
||||
groupedByMeta: getGroupedByMeta(deployment, groupBy),
|
||||
});
|
||||
333
frontend/src/container/InfraMonitoringK8s/Deployments/utils.tsx
Normal file
333
frontend/src/container/InfraMonitoringK8s/Deployments/utils.tsx
Normal file
@@ -0,0 +1,333 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
|
||||
import {
|
||||
K8sDeploymentsData,
|
||||
K8sDeploymentsListPayload,
|
||||
} from 'api/infraMonitoring/getK8sDeploymentsList';
|
||||
import { Group } from 'lucide-react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
EntityProgressBar,
|
||||
formatBytes,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../commonUtils';
|
||||
import { IEntityColumn } from '../utils';
|
||||
|
||||
export const defaultAddedColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Deployment Name',
|
||||
value: 'deploymentName',
|
||||
id: 'deploymentName',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Available',
|
||||
value: 'available_pods',
|
||||
id: 'available_pods',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Desired',
|
||||
value: 'desired_pods',
|
||||
id: 'desired_pods',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Request Utilization (% of limit)',
|
||||
value: 'cpu_request',
|
||||
id: 'cpu_request',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Limit Utilization (% of request)',
|
||||
value: 'cpu_limit',
|
||||
id: 'cpu_limit',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Utilization (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Memory Request Utilization (% of limit)',
|
||||
value: 'memory_request',
|
||||
id: 'memory_request',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Memory Limit Utilization (% of request)',
|
||||
value: 'memory_limit',
|
||||
id: 'memory_limit',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Memory Utilization (bytes)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canRemove: false,
|
||||
},
|
||||
];
|
||||
|
||||
export interface K8sDeploymentsRowData {
|
||||
key: string;
|
||||
deploymentUID: string;
|
||||
deploymentName: React.ReactNode;
|
||||
available_pods: React.ReactNode;
|
||||
desired_pods: React.ReactNode;
|
||||
cpu_request: React.ReactNode;
|
||||
cpu_limit: React.ReactNode;
|
||||
cpu: React.ReactNode;
|
||||
memory_request: React.ReactNode;
|
||||
memory_limit: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
restarts: React.ReactNode;
|
||||
clusterName: string;
|
||||
namespaceName: string;
|
||||
groupedByMeta?: any;
|
||||
}
|
||||
|
||||
const deploymentGroupColumnConfig = {
|
||||
title: (
|
||||
<div className="column-header entity-group-header">
|
||||
<Group size={14} /> DEPLOYMENT GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'deploymentGroup',
|
||||
key: 'deploymentGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
className: 'column entity-group-header',
|
||||
};
|
||||
|
||||
export const getK8sDeploymentsListQuery = (): K8sDeploymentsListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
const columnsConfig = [
|
||||
{
|
||||
title: (
|
||||
<div className="column-header-left deployment-name-header">
|
||||
Deployment Name
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'deploymentName',
|
||||
key: 'deploymentName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className="column-header-left namespace-name-header">
|
||||
Namespace Name
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left small-col">Available</div>,
|
||||
dataIndex: 'available_pods',
|
||||
key: 'available_pods',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left small-col">Desired</div>,
|
||||
dataIndex: 'desired_pods',
|
||||
key: 'desired_pods',
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left med-col">CPU Req Usage (%)</div>,
|
||||
dataIndex: 'cpu_request',
|
||||
key: 'cpu_request',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left med-col">CPU Limit Usage (%)</div>,
|
||||
dataIndex: 'cpu_limit',
|
||||
key: 'cpu_limit',
|
||||
width: 50,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header- small-col">CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left med-col">Mem Req Usage (%)</div>,
|
||||
dataIndex: 'memory_request',
|
||||
key: 'memory_request',
|
||||
width: 50,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left med-col">Mem Limit Usage (%)</div>,
|
||||
dataIndex: 'memory_limit',
|
||||
key: 'memory_limit',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left small-col">Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const getK8sDeploymentsListColumns = (
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): ColumnType<K8sDeploymentsRowData>[] => {
|
||||
if (groupBy.length > 0) {
|
||||
const filteredColumns = [...columnsConfig].filter(
|
||||
(column) => column.key !== 'deploymentName',
|
||||
);
|
||||
filteredColumns.unshift(deploymentGroupColumnConfig);
|
||||
return filteredColumns as ColumnType<K8sDeploymentsRowData>[];
|
||||
}
|
||||
|
||||
return columnsConfig as ColumnType<K8sDeploymentsRowData>[];
|
||||
};
|
||||
|
||||
const dotToUnder: Record<string, keyof K8sDeploymentsData['meta']> = {
|
||||
'k8s.deployment.name': 'k8s_deployment_name',
|
||||
'k8s.namespace.name': 'k8s_namespace_name',
|
||||
'k8s.cluster.name': 'k8s_cluster_name',
|
||||
};
|
||||
|
||||
const getGroupByEle = (
|
||||
deployment: K8sDeploymentsData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): React.ReactNode => {
|
||||
const groupByValues: string[] = [];
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
|
||||
// Choose mapped key if present, otherwise use rawKey
|
||||
const metaKey = (dotToUnder[rawKey] ??
|
||||
rawKey) as keyof typeof deployment.meta;
|
||||
const value = deployment.meta[metaKey];
|
||||
|
||||
groupByValues.push(value);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="pod-group">
|
||||
{groupByValues.map((value) => (
|
||||
<Tag key={value} color={Color.BG_SLATE_400} className="pod-group-tag-item">
|
||||
{value === '' ? '<no-value>' : value}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const formatDataForTable = (
|
||||
data: K8sDeploymentsData[],
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): K8sDeploymentsRowData[] =>
|
||||
data.map((deployment, index) => ({
|
||||
key: index.toString(),
|
||||
deploymentUID: deployment.meta.k8s_deployment_name,
|
||||
deploymentName: (
|
||||
<Tooltip title={deployment.meta.k8s_deployment_name}>
|
||||
{deployment.meta.k8s_deployment_name}
|
||||
</Tooltip>
|
||||
),
|
||||
available_pods: (
|
||||
<ValidateColumnValueWrapper value={deployment.availablePods}>
|
||||
{deployment.availablePods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
desired_pods: (
|
||||
<ValidateColumnValueWrapper value={deployment.desiredPods}>
|
||||
{deployment.desiredPods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
restarts: (
|
||||
<ValidateColumnValueWrapper value={deployment.restarts}>
|
||||
{deployment.restarts}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={deployment.cpuUsage}>
|
||||
{deployment.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper value={deployment.cpuRequest}>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={deployment.cpuRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper value={deployment.cpuLimit}>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={deployment.cpuLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={deployment.memoryUsage}>
|
||||
{formatBytes(deployment.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper value={deployment.memoryRequest}>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={deployment.memoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper value={deployment.memoryLimit}>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={deployment.memoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
clusterName: deployment.meta.k8s_cluster_name,
|
||||
namespaceName: deployment.meta.k8s_namespace_name,
|
||||
deploymentGroup: getGroupByEle(deployment, groupBy),
|
||||
meta: deployment.meta,
|
||||
...deployment.meta,
|
||||
groupedByMeta: deployment.meta,
|
||||
}));
|
||||
@@ -29,7 +29,6 @@ export const QUERY_KEYS = {
|
||||
K8S_STATEFUL_SET_NAME: 'k8s.statefulset.name',
|
||||
K8S_JOB_NAME: 'k8s.job.name',
|
||||
K8S_DAEMON_SET_NAME: 'k8s.daemonset.name',
|
||||
K8S_PERSISTENT_VOLUME_CLAIM_NAME: 'k8s.persistentvolumeclaim.name',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Button, CollapseProps } from 'antd';
|
||||
import type { CollapseProps } from 'antd';
|
||||
import { Collapse, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
Computer,
|
||||
Container,
|
||||
FilePenLine,
|
||||
Filter,
|
||||
Group,
|
||||
HardDrive,
|
||||
Workflow,
|
||||
@@ -64,11 +63,13 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
const [, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
|
||||
const [quickFiltersLastUpdated, setQuickFiltersLastUpdated] = useState(-1);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const handleFilterVisibilityChange = useCallback((): void => {
|
||||
setShowFilters((show) => !show);
|
||||
}, []);
|
||||
const handleFilterVisibilityChange = (): void => {
|
||||
setShowFilters(!showFilters);
|
||||
};
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
@@ -85,6 +86,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
// update the current query with the new filters
|
||||
// in infra monitoring k8s, we are using only one query, hence updating the 0th index of queryData
|
||||
handleChangeQueryData('filters', query.builder.queryData[0].filters);
|
||||
setQuickFiltersLastUpdated(Date.now());
|
||||
setFilters(JSON.stringify(query.builder.queryData[0].filters));
|
||||
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
@@ -319,25 +321,6 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const showFiltersComp = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
{!showFilters && (
|
||||
<div className="quick-filters-toggle-container">
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={handleFilterVisibilityChange}
|
||||
>
|
||||
<Filter size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [handleFilterVisibilityChange, showFilters]);
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className="infra-monitoring-container">
|
||||
@@ -372,39 +355,75 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
}`}
|
||||
>
|
||||
{selectedCategory === K8sCategories.PODS && (
|
||||
<K8sPodLists controlListPrefix={showFiltersComp} />
|
||||
<K8sPodLists
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.NODES && (
|
||||
<K8sNodesList controlListPrefix={showFiltersComp} />
|
||||
<K8sNodesList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.CLUSTERS && (
|
||||
<K8sClustersList controlListPrefix={showFiltersComp} />
|
||||
<K8sClustersList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.DEPLOYMENTS && (
|
||||
<K8sDeploymentsList controlListPrefix={showFiltersComp} />
|
||||
<K8sDeploymentsList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.NAMESPACES && (
|
||||
<K8sNamespacesList controlListPrefix={showFiltersComp} />
|
||||
<K8sNamespacesList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.STATEFULSETS && (
|
||||
<K8sStatefulSetsList controlListPrefix={showFiltersComp} />
|
||||
<K8sStatefulSetsList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.JOBS && (
|
||||
<K8sJobsList controlListPrefix={showFiltersComp} />
|
||||
<K8sJobsList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.DAEMONSETS && (
|
||||
<K8sDaemonSetsList controlListPrefix={showFiltersComp} />
|
||||
<K8sDaemonSetsList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.VOLUMES && (
|
||||
<K8sVolumesList controlListPrefix={showFiltersComp} />
|
||||
<K8sVolumesList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { K8sJobsData } from 'api/infraMonitoring/getK8sJobsList';
|
||||
|
||||
export type JobDetailsProps = {
|
||||
job: K8sJobsData | null;
|
||||
isModalTimeSelection: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -0,0 +1,654 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||
import type { RadioChangeEvent } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import {
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import {
|
||||
BarChart2,
|
||||
ChevronsLeftRight,
|
||||
Compass,
|
||||
DraftingCompass,
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import JobEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
import JobLogs from '../../EntityDetailsUtils/EntityLogs';
|
||||
import JobMetrics from '../../EntityDetailsUtils/EntityMetrics';
|
||||
import JobTraces from '../../EntityDetailsUtils/EntityTraces';
|
||||
import { QUERY_KEYS } from '../../EntityDetailsUtils/utils';
|
||||
import { getJobMetricsQueryPayload, jobWidgetInfo } from './constants';
|
||||
import { JobDetailsProps } from './JobDetails.interfaces';
|
||||
|
||||
import '../../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
|
||||
function JobDetails({
|
||||
job,
|
||||
onClose,
|
||||
isModalTimeSelection,
|
||||
}: JobDetailsProps): JSX.Element {
|
||||
const { maxTime, minTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
|
||||
minTime,
|
||||
]);
|
||||
const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
||||
startTime: startMs,
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [
|
||||
tracesFiltersParam,
|
||||
setTracesFiltersParam,
|
||||
] = useInfraMonitoringTracesFilters();
|
||||
const [
|
||||
eventsFiltersParam,
|
||||
setEventsFiltersParam,
|
||||
] = useInfraMonitoringEventsFilters();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const initialFilters = useMemo(() => {
|
||||
const filters =
|
||||
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
|
||||
if (filters) {
|
||||
return filters;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_JOB_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_job_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: job?.meta.k8s_job_name || '',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_job_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: job?.meta.k8s_namespace_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [
|
||||
job?.meta.k8s_job_name,
|
||||
job?.meta.k8s_namespace_name,
|
||||
selectedView,
|
||||
logFiltersParam,
|
||||
tracesFiltersParam,
|
||||
]);
|
||||
|
||||
const initialEventsFilters = useMemo(() => {
|
||||
if (eventsFiltersParam) {
|
||||
return eventsFiltersParam;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.kind--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: 'Job',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: job?.meta.k8s_job_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [job?.meta.k8s_job_name, eventsFiltersParam]);
|
||||
|
||||
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
|
||||
IBuilderQuery['filters']
|
||||
>(initialFilters);
|
||||
|
||||
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
|
||||
initialEventsFilters,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (job) {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
});
|
||||
}
|
||||
}, [job]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogAndTracesFilters(initialFilters);
|
||||
setEventsFilters(initialEventsFilters);
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
}, [selectedTime, minTime, maxTime]);
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
setLogFiltersParam(null);
|
||||
setTracesFiltersParam(null);
|
||||
setEventsFiltersParam(null);
|
||||
logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
view: e.target.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 / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
interval,
|
||||
view: selectedView,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeLogFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_JOB_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||
item.key?.key ?? '',
|
||||
),
|
||||
);
|
||||
const paginationFilter = value?.items?.find(
|
||||
(item) => item.key?.key === 'id',
|
||||
);
|
||||
const newFilters = value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_JOB_NAME,
|
||||
);
|
||||
|
||||
if (newFilters && newFilters?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
view: 'logs',
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(newFilters || []),
|
||||
...(paginationFilter ? [paginationFilter] : []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setLogFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeTracesFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_JOB_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||
item.key?.key ?? '',
|
||||
),
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
view: 'traces',
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(value?.items?.filter(
|
||||
(item) => item.key?.key !== QUERY_KEYS.K8S_JOB_NAME,
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setTracesFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeEventsFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setEventsFilters((prevFilters) => {
|
||||
const jobKindFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
);
|
||||
const jobNameFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
view: 'events',
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: [
|
||||
jobKindFilter,
|
||||
jobNameFilter,
|
||||
...(value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
};
|
||||
|
||||
setEventsFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleExplorePagesRedirect = (): void => {
|
||||
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: InfraMonitoringEvents.Job,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const filtersWithoutPagination = {
|
||||
...logAndTracesFilters,
|
||||
items:
|
||||
logAndTracesFilters?.items?.filter((item) => item.key?.key !== 'id') || [],
|
||||
};
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.logs,
|
||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||
filters: filtersWithoutPagination,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
aggregateOperator: TracesAggregatorOperator.NOOP,
|
||||
filters: logAndTracesFilters,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
setSelectedView(VIEW_TYPES.METRICS);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="70%"
|
||||
title={
|
||||
<>
|
||||
<Divider type="vertical" />
|
||||
<Typography.Text className="title">
|
||||
{job?.meta.k8s_job_name}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!job}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="entity-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{job && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Job Name
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Namespace Name
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="values-row">
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={job.meta.k8s_job_name}>
|
||||
{job.meta.k8s_job_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={job.meta.k8s_namespace_name}>
|
||||
{job.meta.k8s_namespace_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="views-tabs-container">
|
||||
<Radio.Group
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.METRICS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<BarChart2 size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.LOGS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.LOGS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.TRACES ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.TRACES}
|
||||
>
|
||||
<div className="view-title">
|
||||
<DraftingCompass size={14} />
|
||||
Traces
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.EVENTS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.EVENTS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Events
|
||||
</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
|
||||
{(selectedView === VIEW_TYPES.LOGS ||
|
||||
selectedView === VIEW_TYPES.TRACES) && (
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{selectedView === VIEW_TYPES.METRICS && (
|
||||
<JobMetrics
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
entity={job}
|
||||
category={K8sCategory.JOBS}
|
||||
queryKey="jobMetrics"
|
||||
entityWidgetInfo={jobWidgetInfo}
|
||||
getEntityQueryPayload={getJobMetricsQueryPayload}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<JobLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
category={K8sCategory.JOBS}
|
||||
queryKey="jobLogs"
|
||||
queryKeyFilters={[
|
||||
QUERY_KEYS.K8S_JOB_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<JobTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||
tracesFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="jobTraces"
|
||||
category={InfraMonitoringEvents.Job}
|
||||
queryKeyFilters={[
|
||||
QUERY_KEYS.K8S_JOB_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.EVENTS && (
|
||||
<JobEvents
|
||||
timeRange={modalTimeRange}
|
||||
handleChangeEventFilters={handleChangeEventsFilters}
|
||||
filters={eventsFilters}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
category={K8sCategory.JOBS}
|
||||
queryKey="jobEvents"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default JobDetails;
|
||||
@@ -1,72 +1,11 @@
|
||||
import { K8sJobsData } from 'api/infraMonitoring/getK8sJobsList';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
createFilterItem,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from '../Base/K8sBaseDetails';
|
||||
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
|
||||
import { K8sJobsData } from './api';
|
||||
|
||||
export const k8sJobGetSelectedItemFilters = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_job_name',
|
||||
key: {
|
||||
key: 'k8s_job_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const k8sJobDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sJobsData>[] = [
|
||||
{
|
||||
label: 'Job Name',
|
||||
getValue: (p): string => p.meta.k8s_job_name,
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
getValue: (p): string => p.meta.k8s_cluster_name,
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
getValue: (p): string => p.meta.k8s_namespace_name,
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sJobInitialFilters = [
|
||||
QUERY_KEYS.K8S_JOB_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
];
|
||||
|
||||
export const k8sJobInitialEventsFilter = (
|
||||
item: K8sJobsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Job'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_job_name),
|
||||
];
|
||||
|
||||
export const k8sJobInitialLogTracesFilter = (
|
||||
item: K8sJobsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_JOB_NAME, item.meta.k8s_job_name),
|
||||
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
|
||||
];
|
||||
|
||||
export const k8sJobGetEntityName = (item: K8sJobsData): string =>
|
||||
item.meta.k8s_job_name;
|
||||
|
||||
export const jobWidgetInfo = [
|
||||
{
|
||||
title: 'CPU usage',
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user