mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-05 11:50:19 +01:00
Compare commits
90 Commits
feat/migra
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8158a85e86 | ||
|
|
b7d7a5422e | ||
|
|
74b7f4b4e8 | ||
|
|
985d66539a | ||
|
|
b15f817ba3 | ||
|
|
5e1cf14de9 | ||
|
|
99944b5f92 | ||
|
|
f8eda16533 | ||
|
|
a2eb8ab00a | ||
|
|
601007cba1 | ||
|
|
925a29d2df | ||
|
|
d54fc50236 | ||
|
|
a2ad5b1172 | ||
|
|
802a11ee2b | ||
|
|
a8124f6e73 | ||
|
|
8811aaefe8 | ||
|
|
66aaaea918 | ||
|
|
900c489d91 | ||
|
|
743fe56523 | ||
|
|
3a9e93ebdf | ||
|
|
cdbb78a93d | ||
|
|
c11186f7bf | ||
|
|
51dbb0b5b9 | ||
|
|
2545d7df61 | ||
|
|
3f91821825 | ||
|
|
ee5d182539 | ||
|
|
0bc12f02bc | ||
|
|
e5f00421fe | ||
|
|
539252e10c | ||
|
|
d65f426254 | ||
|
|
6e52f2c8f0 | ||
|
|
d9f8a4ae5a | ||
|
|
eefe3edffd | ||
|
|
2051861a03 | ||
|
|
4b01a40fb9 | ||
|
|
2d8a00bf18 | ||
|
|
f1b26b310f | ||
|
|
2c438b6c32 | ||
|
|
1814c2d13c | ||
|
|
e6cd771f11 | ||
|
|
6b94f87ca0 | ||
|
|
bf315253ae | ||
|
|
668ff7bc39 | ||
|
|
07f2aa52fd | ||
|
|
3416b3ad55 | ||
|
|
d6caa4f2c7 | ||
|
|
f86371566d | ||
|
|
9115803084 | ||
|
|
0c14d8f966 | ||
|
|
7afb461af8 | ||
|
|
a21fbb4ee0 | ||
|
|
0369842f3d | ||
|
|
59cd96562a | ||
|
|
cc4475cab7 | ||
|
|
ac8c648420 | ||
|
|
bede6be4b8 | ||
|
|
dd3d60e6df | ||
|
|
538ab686d2 | ||
|
|
936a325cb9 | ||
|
|
c6cdcd0143 | ||
|
|
cd9211d718 | ||
|
|
0601c28782 | ||
|
|
580610dbfa | ||
|
|
2d2aa02a81 | ||
|
|
dd9723ad13 | ||
|
|
3651469416 | ||
|
|
febce75734 | ||
|
|
e1616f3487 | ||
|
|
4b94287ac7 | ||
|
|
1575c7c54c | ||
|
|
8def3f835b | ||
|
|
11ed15f4c5 | ||
|
|
f47877cca9 | ||
|
|
bb2b9215ba | ||
|
|
3111904223 | ||
|
|
003e2c30d8 | ||
|
|
00fe516d10 | ||
|
|
0305f4f7db | ||
|
|
c60019a6dc | ||
|
|
acde2a37fa | ||
|
|
945241a52a | ||
|
|
e967f80c86 | ||
|
|
a09dc325de | ||
|
|
379b4f7fc4 | ||
|
|
5e536ae077 | ||
|
|
234585e642 | ||
|
|
2cc14f1ad4 | ||
|
|
dc4ed4d239 | ||
|
|
7281c36873 | ||
|
|
40288776e8 |
@@ -19,9 +19,12 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/gateway/noopgateway"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
@@ -29,6 +32,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
"github.com/SigNoz/signoz/pkg/zeus/noopzeus"
|
||||
@@ -96,6 +100,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
|
||||
return querier.NewHandler(ps, q, a)
|
||||
},
|
||||
func(_ cloudintegrationtypes.Store, _ zeus.Zeus, _ gateway.Gateway, _ licensing.Licensing, _ serviceaccount.Module) (cloudintegration.Module, error) {
|
||||
return implcloudintegration.NewModule(), nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "failed to create signoz", errors.Attr(err))
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/gateway/httpgateway"
|
||||
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
|
||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||
"github.com/SigNoz/signoz/ee/modules/cloudintegration/implcloudintegration"
|
||||
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
|
||||
eequerier "github.com/SigNoz/signoz/ee/querier"
|
||||
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
|
||||
@@ -30,9 +31,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/gateway"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
@@ -40,6 +43,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
)
|
||||
@@ -125,7 +129,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
return nil, err
|
||||
}
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, dashboardModule), nil
|
||||
|
||||
},
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
|
||||
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)
|
||||
@@ -137,8 +140,10 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
communityHandler := querier.NewHandler(ps, q, a)
|
||||
return eequerier.NewHandler(ps, q, communityHandler)
|
||||
},
|
||||
func(store cloudintegrationtypes.Store, zeus zeus.Zeus, gateway gateway.Gateway, licensing licensing.Licensing, serviceAccount serviceaccount.Module) (cloudintegration.Module, error) {
|
||||
return implcloudintegration.NewModule(store, config.Global, zeus, gateway, licensing, serviceAccount)
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "failed to create signoz", errors.Attr(err))
|
||||
return err
|
||||
|
||||
@@ -421,11 +421,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 +465,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 +478,7 @@ components:
|
||||
type: object
|
||||
CloudintegrationtypesAWSMetricsStrategy:
|
||||
properties:
|
||||
cloudwatch_metric_stream_filters:
|
||||
cloudwatchMetricStreamFilters:
|
||||
items:
|
||||
properties:
|
||||
MetricNames:
|
||||
@@ -577,6 +577,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:
|
||||
@@ -710,11 +730,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 +806,8 @@ components:
|
||||
properties:
|
||||
assets:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesAssets'
|
||||
cloudIntegrationService:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesCloudIntegrationService'
|
||||
dataCollected:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesDataCollected'
|
||||
icon:
|
||||
@@ -751,9 +816,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 +828,10 @@ components:
|
||||
- icon
|
||||
- overview
|
||||
- assets
|
||||
- supported_signals
|
||||
- supportedSignals
|
||||
- dataCollected
|
||||
- telemetryCollectionStrategy
|
||||
- cloudIntegrationService
|
||||
type: object
|
||||
CloudintegrationtypesServiceConfig:
|
||||
properties:
|
||||
@@ -776,6 +840,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:
|
||||
@@ -2313,6 +2393,15 @@ components:
|
||||
- status
|
||||
- error
|
||||
type: object
|
||||
RulestatehistorytypesAlertState:
|
||||
enum:
|
||||
- inactive
|
||||
- pending
|
||||
- recovering
|
||||
- firing
|
||||
- nodata
|
||||
- disabled
|
||||
type: string
|
||||
RulestatehistorytypesGettableRuleStateHistory:
|
||||
properties:
|
||||
fingerprint:
|
||||
@@ -2324,15 +2413,15 @@ components:
|
||||
nullable: true
|
||||
type: array
|
||||
overallState:
|
||||
$ref: '#/components/schemas/RuletypesAlertState'
|
||||
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
|
||||
overallStateChanged:
|
||||
type: boolean
|
||||
ruleId:
|
||||
ruleID:
|
||||
type: string
|
||||
ruleName:
|
||||
type: string
|
||||
state:
|
||||
$ref: '#/components/schemas/RuletypesAlertState'
|
||||
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
|
||||
stateChanged:
|
||||
type: boolean
|
||||
unixMilli:
|
||||
@@ -2342,7 +2431,7 @@ components:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- ruleId
|
||||
- ruleID
|
||||
- ruleName
|
||||
- overallState
|
||||
- overallStateChanged
|
||||
@@ -2432,21 +2521,12 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
state:
|
||||
$ref: '#/components/schemas/RuletypesAlertState'
|
||||
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
|
||||
required:
|
||||
- state
|
||||
- start
|
||||
- end
|
||||
type: object
|
||||
RuletypesAlertState:
|
||||
enum:
|
||||
- inactive
|
||||
- pending
|
||||
- recovering
|
||||
- firing
|
||||
- nodata
|
||||
- disabled
|
||||
type: string
|
||||
ServiceaccounttypesGettableFactorAPIKey:
|
||||
properties:
|
||||
createdAt:
|
||||
@@ -3410,6 +3490,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
|
||||
@@ -3577,55 +3712,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
|
||||
@@ -8469,7 +8555,7 @@ paths:
|
||||
- in: query
|
||||
name: state
|
||||
schema:
|
||||
$ref: '#/components/schemas/RuletypesAlertState'
|
||||
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
|
||||
- in: query
|
||||
name: filterExpression
|
||||
schema:
|
||||
|
||||
@@ -32,7 +32,7 @@ func (s Seasonality) IsValid() bool {
|
||||
}
|
||||
|
||||
type AnomaliesRequest struct {
|
||||
Params *qbtypes.QueryRangeRequest
|
||||
Params qbtypes.QueryRangeRequest
|
||||
Seasonality Seasonality
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ type anomalyQueryParams struct {
|
||||
Past3SeasonQuery qbtypes.QueryRangeRequest
|
||||
}
|
||||
|
||||
func prepareAnomalyQueryParams(req *qbtypes.QueryRangeRequest, seasonality Seasonality) *anomalyQueryParams {
|
||||
func prepareAnomalyQueryParams(req qbtypes.QueryRangeRequest, seasonality Seasonality) *anomalyQueryParams {
|
||||
start := req.Start
|
||||
end := req.End
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package implcloudprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sort"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
)
|
||||
|
||||
type awscloudprovider struct {
|
||||
serviceDefinitions cloudintegrationtypes.ServiceDefinitionStore
|
||||
}
|
||||
|
||||
func NewAWSCloudProvider(defStore cloudintegrationtypes.ServiceDefinitionStore) (cloudintegration.CloudProviderModule, error) {
|
||||
return &awscloudprovider{serviceDefinitions: defStore}, nil
|
||||
}
|
||||
|
||||
func (provider *awscloudprovider) GetConnectionArtifact(ctx context.Context, creds *cloudintegrationtypes.SignozCredentials, account *cloudintegrationtypes.Account, req *cloudintegrationtypes.ConnectionArtifactRequest) (*cloudintegrationtypes.ConnectionArtifact, error) {
|
||||
// TODO: get this from config
|
||||
agentVersion := "v0.0.8"
|
||||
|
||||
baseURL := fmt.Sprintf("https://%s.console.aws.amazon.com/cloudformation/home", req.Aws.DeploymentRegion)
|
||||
u, _ := url.Parse(baseURL)
|
||||
|
||||
q := u.Query()
|
||||
q.Set("region", req.Aws.DeploymentRegion)
|
||||
u.Fragment = "/stacks/quickcreate"
|
||||
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
q = u.Query()
|
||||
q.Set("stackName", "signoz-integration")
|
||||
q.Set("templateURL", fmt.Sprintf("https://signoz-integrations.s3.us-east-1.amazonaws.com/aws-quickcreate-template-%s.json", agentVersion))
|
||||
q.Set("param_SigNozIntegrationAgentVersion", agentVersion)
|
||||
q.Set("param_SigNozApiUrl", creds.SigNozAPIURL)
|
||||
q.Set("param_SigNozApiKey", creds.SigNozAPIKey)
|
||||
q.Set("param_SigNozAccountId", account.ID.StringValue())
|
||||
q.Set("param_IngestionUrl", creds.IngestionURL)
|
||||
q.Set("param_IngestionKey", creds.IngestionKey)
|
||||
|
||||
return &cloudintegrationtypes.ConnectionArtifact{
|
||||
Aws: &cloudintegrationtypes.AWSConnectionArtifact{
|
||||
ConnectionURL: u.String() + "?&" + q.Encode(), // this format is required by AWS
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *awscloudprovider) ListServiceDefinitions(ctx context.Context) ([]*cloudintegrationtypes.ServiceDefinition, error) {
|
||||
return provider.serviceDefinitions.List(ctx, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
}
|
||||
|
||||
func (provider *awscloudprovider) GetServiceDefinition(ctx context.Context, serviceID cloudintegrationtypes.ServiceID) (*cloudintegrationtypes.ServiceDefinition, error) {
|
||||
return provider.serviceDefinitions.Get(ctx, cloudintegrationtypes.CloudProviderTypeAWS, serviceID)
|
||||
}
|
||||
|
||||
func (provider *awscloudprovider) StorableConfigFromServiceConfig(ctx context.Context, cfg *cloudintegrationtypes.ServiceConfig, supported cloudintegrationtypes.SupportedSignals) (string, error) {
|
||||
if cfg == nil || cfg.AWS == nil {
|
||||
return "", nil
|
||||
}
|
||||
// Strip signal configs the service does not support before storing.
|
||||
if !supported.Logs {
|
||||
cfg.AWS.Logs = nil
|
||||
}
|
||||
if !supported.Metrics {
|
||||
cfg.AWS.Metrics = nil
|
||||
}
|
||||
b, err := json.Marshal(cfg.AWS)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (provider *awscloudprovider) ServiceConfigFromStorableServiceConfig(ctx context.Context, config string) (*cloudintegrationtypes.ServiceConfig, error) {
|
||||
if config == "" {
|
||||
return nil, errors.NewInternalf(errors.CodeInternal, "service config is empty")
|
||||
}
|
||||
|
||||
var awsCfg cloudintegrationtypes.AWSServiceConfig
|
||||
if err := json.Unmarshal([]byte(config), &awsCfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cloudintegrationtypes.ServiceConfig{AWS: &awsCfg}, nil
|
||||
}
|
||||
|
||||
func (provider *awscloudprovider) IsServiceEnabled(ctx context.Context, config *cloudintegrationtypes.ServiceConfig) bool {
|
||||
if config == nil || config.AWS == nil {
|
||||
return false
|
||||
}
|
||||
logsEnabled := config.AWS.Logs != nil && config.AWS.Logs.Enabled
|
||||
metricsEnabled := config.AWS.Metrics != nil && config.AWS.Metrics.Enabled
|
||||
return logsEnabled || metricsEnabled
|
||||
}
|
||||
|
||||
func (provider *awscloudprovider) IsMetricsEnabled(ctx context.Context, config *cloudintegrationtypes.ServiceConfig) bool {
|
||||
if config == nil || config.AWS == nil {
|
||||
return false
|
||||
}
|
||||
return awsMetricsEnabled(config.AWS)
|
||||
}
|
||||
|
||||
func (provider *awscloudprovider) BuildIntegrationConfig(
|
||||
ctx context.Context,
|
||||
account *cloudintegrationtypes.Account,
|
||||
services []*cloudintegrationtypes.StorableCloudIntegrationService,
|
||||
) (*cloudintegrationtypes.ProviderIntegrationConfig, error) {
|
||||
// Sort services for deterministic output
|
||||
sort.Slice(services, func(i, j int) bool {
|
||||
return services[i].Type.StringValue() < services[j].Type.StringValue()
|
||||
})
|
||||
|
||||
compiledMetrics := &cloudintegrationtypes.AWSMetricsStrategy{}
|
||||
compiledLogs := &cloudintegrationtypes.AWSLogsStrategy{}
|
||||
var compiledS3Buckets map[string][]string
|
||||
|
||||
for _, storedSvc := range services {
|
||||
svcCfg, err := provider.ServiceConfigFromStorableServiceConfig(ctx, storedSvc.Config)
|
||||
if err != nil || svcCfg == nil || svcCfg.AWS == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
svcDef, err := provider.GetServiceDefinition(ctx, storedSvc.Type)
|
||||
if err != nil || svcDef == nil || svcDef.Strategy == nil || svcDef.Strategy.AWS == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
strategy := svcDef.Strategy.AWS
|
||||
|
||||
// S3Sync: logs come directly from configured S3 buckets, not CloudWatch subscriptions
|
||||
if storedSvc.Type == cloudintegrationtypes.AWSServiceS3Sync {
|
||||
if awsLogsEnabled(svcCfg.AWS) && svcCfg.AWS.Logs.S3Buckets != nil {
|
||||
compiledS3Buckets = svcCfg.AWS.Logs.S3Buckets
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if awsLogsEnabled(svcCfg.AWS) && strategy.Logs != nil {
|
||||
compiledLogs.Subscriptions = append(compiledLogs.Subscriptions, strategy.Logs.Subscriptions...)
|
||||
}
|
||||
|
||||
if awsMetricsEnabled(svcCfg.AWS) && strategy.Metrics != nil {
|
||||
compiledMetrics.StreamFilters = append(compiledMetrics.StreamFilters, strategy.Metrics.StreamFilters...)
|
||||
}
|
||||
}
|
||||
|
||||
awsTelemetry := &cloudintegrationtypes.AWSCollectionStrategy{}
|
||||
if len(compiledMetrics.StreamFilters) > 0 {
|
||||
awsTelemetry.Metrics = compiledMetrics
|
||||
}
|
||||
if len(compiledLogs.Subscriptions) > 0 {
|
||||
awsTelemetry.Logs = compiledLogs
|
||||
}
|
||||
if compiledS3Buckets != nil {
|
||||
awsTelemetry.S3Buckets = compiledS3Buckets
|
||||
}
|
||||
|
||||
enabledRegions := []string{}
|
||||
if account.Config != nil && account.Config.AWS != nil && account.Config.AWS.Regions != nil {
|
||||
enabledRegions = account.Config.AWS.Regions
|
||||
}
|
||||
|
||||
return &cloudintegrationtypes.ProviderIntegrationConfig{
|
||||
AWS: &cloudintegrationtypes.AWSIntegrationConfig{
|
||||
EnabledRegions: enabledRegions,
|
||||
Telemetry: awsTelemetry,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// awsLogsEnabled returns true if the AWS service config has logs explicitly enabled.
|
||||
func awsLogsEnabled(cfg *cloudintegrationtypes.AWSServiceConfig) bool {
|
||||
return cfg.Logs != nil && cfg.Logs.Enabled
|
||||
}
|
||||
|
||||
// awsMetricsEnabled returns true if the AWS service config has metrics explicitly enabled.
|
||||
func awsMetricsEnabled(cfg *cloudintegrationtypes.AWSServiceConfig) bool {
|
||||
return cfg.Metrics != nil && cfg.Metrics.Enabled
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package implcloudprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
)
|
||||
|
||||
type azurecloudprovider struct{}
|
||||
|
||||
func NewAzureCloudProvider() cloudintegration.CloudProviderModule {
|
||||
return &azurecloudprovider{}
|
||||
}
|
||||
|
||||
func (provider *azurecloudprovider) GetConnectionArtifact(ctx context.Context, creds *cloudintegrationtypes.SignozCredentials, account *cloudintegrationtypes.Account, req *cloudintegrationtypes.ConnectionArtifactRequest) (*cloudintegrationtypes.ConnectionArtifact, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (provider *azurecloudprovider) ListServiceDefinitions(ctx context.Context) ([]*cloudintegrationtypes.ServiceDefinition, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (provider *azurecloudprovider) GetServiceDefinition(ctx context.Context, serviceID cloudintegrationtypes.ServiceID) (*cloudintegrationtypes.ServiceDefinition, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (provider *azurecloudprovider) StorableConfigFromServiceConfig(ctx context.Context, cfg *cloudintegrationtypes.ServiceConfig, supported cloudintegrationtypes.SupportedSignals) (string, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (provider *azurecloudprovider) ServiceConfigFromStorableServiceConfig(ctx context.Context, config string) (*cloudintegrationtypes.ServiceConfig, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (provider *azurecloudprovider) IsServiceEnabled(ctx context.Context, config *cloudintegrationtypes.ServiceConfig) bool {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (provider *azurecloudprovider) IsMetricsEnabled(ctx context.Context, config *cloudintegrationtypes.ServiceConfig) bool {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (provider *azurecloudprovider) BuildIntegrationConfig(
|
||||
ctx context.Context,
|
||||
account *cloudintegrationtypes.Account,
|
||||
services []*cloudintegrationtypes.StorableCloudIntegrationService,
|
||||
) (*cloudintegrationtypes.ProviderIntegrationConfig, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
533
ee/modules/cloudintegration/implcloudintegration/module.go
Normal file
533
ee/modules/cloudintegration/implcloudintegration/module.go
Normal file
@@ -0,0 +1,533 @@
|
||||
package implcloudintegration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/modules/cloudintegration/implcloudintegration/implcloudprovider"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/gateway"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
pkgimpl "github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
)
|
||||
|
||||
type module struct {
|
||||
store cloudintegrationtypes.Store
|
||||
gateway gateway.Gateway
|
||||
zeus zeus.Zeus
|
||||
licensing licensing.Licensing
|
||||
globalConfig global.Config
|
||||
serviceAccount serviceaccount.Module
|
||||
cloudProvidersMap map[cloudintegrationtypes.CloudProviderType]cloudintegration.CloudProviderModule
|
||||
}
|
||||
|
||||
func NewModule(
|
||||
store cloudintegrationtypes.Store,
|
||||
globalConfig global.Config,
|
||||
zeus zeus.Zeus,
|
||||
gateway gateway.Gateway,
|
||||
licensing licensing.Licensing,
|
||||
serviceAccount serviceaccount.Module,
|
||||
) (cloudintegration.Module, error) {
|
||||
defStore := pkgimpl.NewServiceDefinitionStore()
|
||||
awsCloudProviderModule, err := implcloudprovider.NewAWSCloudProvider(defStore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
azureCloudProviderModule := implcloudprovider.NewAzureCloudProvider()
|
||||
cloudProvidersMap := map[cloudintegrationtypes.CloudProviderType]cloudintegration.CloudProviderModule{
|
||||
cloudintegrationtypes.CloudProviderTypeAWS: awsCloudProviderModule,
|
||||
cloudintegrationtypes.CloudProviderTypeAzure: azureCloudProviderModule,
|
||||
}
|
||||
|
||||
return &module{
|
||||
store: store,
|
||||
globalConfig: globalConfig,
|
||||
zeus: zeus,
|
||||
gateway: gateway,
|
||||
licensing: licensing,
|
||||
serviceAccount: serviceAccount,
|
||||
cloudProvidersMap: cloudProvidersMap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (module *module) CreateAccount(ctx context.Context, account *cloudintegrationtypes.Account) error {
|
||||
_, err := module.licensing.GetActive(ctx, account.OrgID)
|
||||
if err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
storableCloudIntegration, err := cloudintegrationtypes.NewStorableCloudIntegration(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.CreateAccount(ctx, storableCloudIntegration)
|
||||
}
|
||||
|
||||
func (module *module) GetConnectionArtifact(ctx context.Context, account *cloudintegrationtypes.Account, req *cloudintegrationtypes.ConnectionArtifactRequest) (*cloudintegrationtypes.ConnectionArtifact, error) {
|
||||
// TODO: evaluate if this check is really required and remove if the deployment promises to always have this configured.
|
||||
if module.globalConfig.IngestionURL == nil {
|
||||
return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "ingestion URL is not configured")
|
||||
}
|
||||
|
||||
// get license to get the deployment details
|
||||
license, err := module.licensing.GetActive(ctx, account.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get deployment details from zeus
|
||||
respBytes, err := module.zeus.GetDeployment(ctx, license.Key)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't get deployment")
|
||||
}
|
||||
|
||||
// parse deployment details
|
||||
deployment, err := zeustypes.NewGettableDeployment(respBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiKey, err := module.getOrCreateAPIKey(ctx, account.OrgID, account.Provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ingestionKey, err := module.getOrCreateIngestionKey(ctx, account.OrgID, account.Provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
creds := &cloudintegrationtypes.SignozCredentials{
|
||||
SigNozAPIURL: deployment.SignozAPIUrl,
|
||||
SigNozAPIKey: apiKey,
|
||||
IngestionURL: module.globalConfig.IngestionURL.String(),
|
||||
IngestionKey: ingestionKey,
|
||||
}
|
||||
|
||||
cloudProviderModule, err := module.GetCloudProvider(account.Provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cloudProviderModule.GetConnectionArtifact(ctx, creds, account, req)
|
||||
}
|
||||
|
||||
func (module *module) GetAccount(ctx context.Context, orgID valuer.UUID, accountID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.Account, error) {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
storableAccount, err := module.store.GetAccountByID(ctx, orgID, accountID, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cloudintegrationtypes.NewAccountFromStorable(storableAccount)
|
||||
}
|
||||
|
||||
// ListAccounts return only agent connected accounts.
|
||||
func (module *module) ListAccounts(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) ([]*cloudintegrationtypes.Account, error) {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
storableAccounts, err := module.store.ListConnectedAccounts(ctx, orgID, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cloudintegrationtypes.NewAccountsFromStorables(storableAccounts)
|
||||
}
|
||||
|
||||
func (module *module) AgentCheckIn(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, req *cloudintegrationtypes.AgentCheckInRequest) (*cloudintegrationtypes.AgentCheckInResponse, error) {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
connectedAccount, err := module.store.GetConnectedAccount(ctx, orgID, provider, req.ProviderAccountID)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If a different integration is already connected to this provider account ID, reject the check-in.
|
||||
// Allow re-check-in from the same integration (e.g. agent restarting).
|
||||
if connectedAccount != nil && connectedAccount.ID != req.CloudIntegrationID {
|
||||
errMessage := fmt.Sprintf("provider account id %s is already connected to cloud integration id %s", req.ProviderAccountID, connectedAccount.ID)
|
||||
return nil, errors.New(errors.TypeAlreadyExists, cloudintegrationtypes.ErrCodeCloudIntegrationAlreadyConnected, errMessage)
|
||||
}
|
||||
|
||||
account, err := module.store.GetAccountByID(ctx, orgID, req.CloudIntegrationID, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
account.AccountID = &req.ProviderAccountID
|
||||
account.LastAgentReport = &cloudintegrationtypes.StorableAgentReport{
|
||||
TimestampMillis: time.Now().UnixMilli(),
|
||||
Data: req.Data,
|
||||
}
|
||||
|
||||
err = module.store.UpdateAccount(ctx, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If account has been removed (disconnected), return a minimal response with empty integration config.
|
||||
// The agent doesn't act on config for removed accounts.
|
||||
if account.RemovedAt != nil {
|
||||
return &cloudintegrationtypes.AgentCheckInResponse{
|
||||
CloudIntegrationID: account.ID.StringValue(),
|
||||
ProviderAccountID: req.ProviderAccountID,
|
||||
IntegrationConfig: &cloudintegrationtypes.ProviderIntegrationConfig{},
|
||||
RemovedAt: account.RemovedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get account as domain object for config access (enabled regions, etc.)
|
||||
accountDomain, err := cloudintegrationtypes.NewAccountFromStorable(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cloudProvider, err := module.GetCloudProvider(provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
storedServices, err := module.store.ListServices(ctx, req.CloudIntegrationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Delegate integration config building entirely to the provider module
|
||||
integrationConfig, err := cloudProvider.BuildIntegrationConfig(ctx, accountDomain, storedServices)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cloudintegrationtypes.AgentCheckInResponse{
|
||||
CloudIntegrationID: account.ID.StringValue(),
|
||||
ProviderAccountID: req.ProviderAccountID,
|
||||
IntegrationConfig: integrationConfig,
|
||||
RemovedAt: account.RemovedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (module *module) UpdateAccount(ctx context.Context, account *cloudintegrationtypes.Account) error {
|
||||
_, err := module.licensing.GetActive(ctx, account.OrgID)
|
||||
if err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
storableAccount, err := cloudintegrationtypes.NewStorableCloudIntegration(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.UpdateAccount(ctx, storableAccount)
|
||||
}
|
||||
|
||||
func (module *module) DisconnectAccount(ctx context.Context, orgID valuer.UUID, accountID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) error {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
return module.store.RemoveAccount(ctx, orgID, accountID, provider)
|
||||
}
|
||||
|
||||
func (module *module) ListServicesMetadata(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, integrationID *valuer.UUID) ([]*cloudintegrationtypes.ServiceMetadata, error) {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
cloudProvider, err := module.GetCloudProvider(provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceDefinitions, err := cloudProvider.ListServiceDefinitions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
enabledServiceIDs := map[string]bool{}
|
||||
if integrationID != nil {
|
||||
_, err := module.store.GetAccountByID(ctx, orgID, *integrationID, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
storedServices, err := module.store.ListServices(ctx, *integrationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, svc := range storedServices {
|
||||
serviceConfig, err := cloudProvider.ServiceConfigFromStorableServiceConfig(ctx, svc.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cloudProvider.IsServiceEnabled(ctx, serviceConfig) {
|
||||
enabledServiceIDs[svc.Type.StringValue()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp := make([]*cloudintegrationtypes.ServiceMetadata, 0, len(serviceDefinitions))
|
||||
for _, serviceDefinition := range serviceDefinitions {
|
||||
resp = append(resp, cloudintegrationtypes.NewServiceMetadata(*serviceDefinition, enabledServiceIDs[serviceDefinition.ID]))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (module *module) GetService(ctx context.Context, orgID valuer.UUID, integrationID *valuer.UUID, serviceID cloudintegrationtypes.ServiceID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.Service, error) {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
cloudProvider, err := module.GetCloudProvider(provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceDefinition, err := cloudProvider.GetServiceDefinition(ctx, serviceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var integrationService *cloudintegrationtypes.CloudIntegrationService
|
||||
|
||||
if integrationID != nil {
|
||||
_, err := module.store.GetAccountByID(ctx, orgID, *integrationID, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
storedService, err := module.store.GetServiceByServiceID(ctx, *integrationID, serviceID)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
if storedService != nil {
|
||||
serviceConfig, err := cloudProvider.ServiceConfigFromStorableServiceConfig(ctx, storedService.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
integrationService = cloudintegrationtypes.NewCloudIntegrationServiceFromStorable(storedService, serviceConfig)
|
||||
}
|
||||
}
|
||||
|
||||
return cloudintegrationtypes.NewService(*serviceDefinition, integrationService), nil
|
||||
}
|
||||
|
||||
func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
cloudProvider, err := module.GetCloudProvider(provider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceDefinition, err := cloudProvider.GetServiceDefinition(ctx, service.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configJSON, err := cloudProvider.StorableConfigFromServiceConfig(ctx, service.Config, serviceDefinition.SupportedSignals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.CreateService(ctx, cloudintegrationtypes.NewStorableCloudIntegrationService(service, configJSON))
|
||||
}
|
||||
|
||||
func (module *module) UpdateService(ctx context.Context, orgID valuer.UUID, integrationService *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
cloudProvider, err := module.GetCloudProvider(provider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceDefinition, err := cloudProvider.GetServiceDefinition(ctx, integrationService.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configJSON, err := cloudProvider.StorableConfigFromServiceConfig(ctx, integrationService.Config, serviceDefinition.SupportedSignals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storableService := cloudintegrationtypes.NewStorableCloudIntegrationService(integrationService, configJSON)
|
||||
|
||||
return module.store.UpdateService(ctx, storableService)
|
||||
}
|
||||
|
||||
// TODO: use the function in dashboard APIs during removal of older cloud integration code.
|
||||
func (module *module) listDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
|
||||
var allDashboards []*dashboardtypes.Dashboard
|
||||
|
||||
for provider := range module.cloudProvidersMap {
|
||||
cloudProvider, err := module.GetCloudProvider(provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
connectedAccounts, err := module.store.ListConnectedAccounts(ctx, orgID, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, storableAccount := range connectedAccounts {
|
||||
storedServices, err := module.store.ListServices(ctx, storableAccount.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, storedSvc := range storedServices {
|
||||
serviceConfig, err := cloudProvider.ServiceConfigFromStorableServiceConfig(ctx, storedSvc.Config)
|
||||
if err != nil || !cloudProvider.IsMetricsEnabled(ctx, serviceConfig) {
|
||||
continue
|
||||
}
|
||||
|
||||
svcDef, err := cloudProvider.GetServiceDefinition(ctx, storedSvc.Type)
|
||||
if err != nil || svcDef == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
dashboards := cloudintegrationtypes.GetDashboardsFromAssets(
|
||||
storedSvc.Type.StringValue(),
|
||||
orgID,
|
||||
provider,
|
||||
storableAccount.CreatedAt,
|
||||
svcDef.Assets,
|
||||
)
|
||||
allDashboards = append(allDashboards, dashboards...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(allDashboards, func(i, j int) bool {
|
||||
return allDashboards[i].ID < allDashboards[j].ID
|
||||
})
|
||||
|
||||
return allDashboards, nil
|
||||
}
|
||||
|
||||
// TODO: use the function in dashboard APIs during removal of older cloud integration code.
|
||||
func (module *module) GetDashboardByID(ctx context.Context, orgID valuer.UUID, id string) (*dashboardtypes.Dashboard, error) {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
_, _, _, err = cloudintegrationtypes.ParseCloudIntegrationDashboardID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allDashboards, err := module.listDashboards(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, d := range allDashboards {
|
||||
if d.ID == id {
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New(errors.TypeNotFound, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "cloud integration dashboard not found")
|
||||
}
|
||||
|
||||
// TODO: use the function in dashboard APIs during removal of older cloud integration code.
|
||||
func (module *module) ListDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
return module.listDashboards(ctx, orgID)
|
||||
}
|
||||
|
||||
func (module *module) GetCloudProvider(provider cloudintegrationtypes.CloudProviderType) (cloudintegration.CloudProviderModule, error) {
|
||||
if cloudProviderModule, ok := module.cloudProvidersMap[provider]; ok {
|
||||
return cloudProviderModule, nil
|
||||
}
|
||||
|
||||
return nil, errors.NewInvalidInputf(cloudintegrationtypes.ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
|
||||
}
|
||||
|
||||
func (module *module) getOrCreateIngestionKey(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (string, error) {
|
||||
keyName := cloudintegrationtypes.NewIngestionKeyName(provider)
|
||||
|
||||
result, err := module.gateway.SearchIngestionKeysByName(ctx, orgID, keyName, 1, 10)
|
||||
if err != nil {
|
||||
return "", errors.WrapInternalf(err, errors.CodeInternal, "couldn't search ingestion keys")
|
||||
}
|
||||
|
||||
// ideally there should be only one key per cloud integration provider
|
||||
if len(result.Keys) > 0 {
|
||||
return result.Keys[0].Value, nil
|
||||
}
|
||||
|
||||
createdIngestionKey, err := module.gateway.CreateIngestionKey(ctx, orgID, keyName, []string{"integration"}, time.Time{})
|
||||
if err != nil {
|
||||
return "", errors.WrapInternalf(err, errors.CodeInternal, "couldn't create ingestion key")
|
||||
}
|
||||
|
||||
return createdIngestionKey.Value, nil
|
||||
}
|
||||
|
||||
func (module *module) getOrCreateAPIKey(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (string, error) {
|
||||
domain := module.serviceAccount.Config().Email.Domain
|
||||
serviceAccount := serviceaccounttypes.NewServiceAccount("integration", domain, serviceaccounttypes.ServiceAccountStatusActive, orgID)
|
||||
serviceAccount, err := module.serviceAccount.GetOrCreate(ctx, orgID, serviceAccount)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = module.serviceAccount.SetRoleByName(ctx, orgID, serviceAccount.ID, authtypes.SigNozViewerRoleName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
factorAPIKey, err := serviceAccount.NewFactorAPIKey(provider.StringValue(), 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
factorAPIKey, err = module.serviceAccount.GetOrCreateFactorAPIKey(ctx, factorAPIKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return factorAPIKey.Key, nil
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func (h *handler) QueryRange(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
if anomalyQuery, ok := queryRangeRequest.IsAnomalyRequest(); ok {
|
||||
anomalies, err := h.handleAnomalyQuery(ctx, orgID, anomalyQuery, &queryRangeRequest)
|
||||
anomalies, err := h.handleAnomalyQuery(ctx, orgID, anomalyQuery, queryRangeRequest)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.NewInternalf(errors.CodeInternal, "failed to get anomalies: %v", err))
|
||||
return
|
||||
@@ -149,7 +149,7 @@ func (h *handler) createAnomalyProvider(seasonality anomalyV2.Seasonality) anoma
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) handleAnomalyQuery(ctx context.Context, orgID valuer.UUID, anomalyQuery *qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation], queryRangeRequest *qbtypes.QueryRangeRequest) (*anomalyV2.AnomaliesResponse, error) {
|
||||
func (h *handler) handleAnomalyQuery(ctx context.Context, orgID valuer.UUID, anomalyQuery *qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation], queryRangeRequest qbtypes.QueryRangeRequest) (*anomalyV2.AnomaliesResponse, error) {
|
||||
seasonality := extractSeasonality(anomalyQuery)
|
||||
provider := h.createAnomalyProvider(seasonality)
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ import (
|
||||
opAmpModel "github.com/SigNoz/signoz/pkg/query-service/app/opamp/model"
|
||||
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
|
||||
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
)
|
||||
@@ -98,6 +99,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
)
|
||||
|
||||
rm, err := makeRulesManager(
|
||||
reader,
|
||||
signoz.Cache,
|
||||
signoz.Alertmanager,
|
||||
signoz.SQLStore,
|
||||
@@ -343,7 +345,7 @@ func (s *Server) Stop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeRulesManager(cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, ruleStateHistoryModule rulestatehistory.Module, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
|
||||
func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, ruleStateHistoryModule rulestatehistory.Module, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
|
||||
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
||||
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
|
||||
// create manager opts
|
||||
@@ -352,6 +354,7 @@ func makeRulesManager(cache cache.Cache, alertmanager alertmanager.Alertmanager,
|
||||
MetadataStore: metadataStore,
|
||||
Prometheus: prometheus,
|
||||
Context: context.Background(),
|
||||
Reader: ch,
|
||||
Querier: querier,
|
||||
Logger: providerSettings.Logger,
|
||||
Cache: cache,
|
||||
@@ -362,7 +365,7 @@ func makeRulesManager(cache cache.Cache, alertmanager alertmanager.Alertmanager,
|
||||
OrgGetter: orgGetter,
|
||||
RuleStore: ruleStore,
|
||||
MaintenanceStore: maintenanceStore,
|
||||
SQLStore: sqlstore,
|
||||
SqlStore: sqlstore,
|
||||
QueryParser: queryParser,
|
||||
RuleStateHistoryModule: ruleStateHistoryModule,
|
||||
}
|
||||
|
||||
@@ -5,34 +5,58 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/anomaly"
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/transition"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
querierV2 "github.com/SigNoz/signoz/pkg/query-service/app/querier/v2"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/units"
|
||||
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/anomaly"
|
||||
querierV5 "github.com/SigNoz/signoz/pkg/querier"
|
||||
|
||||
anomalyV2 "github.com/SigNoz/signoz/ee/anomaly"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
|
||||
const (
|
||||
RuleTypeAnomaly = "anomaly_rule"
|
||||
)
|
||||
|
||||
type AnomalyRule struct {
|
||||
*baserules.BaseRule
|
||||
|
||||
mtx sync.Mutex
|
||||
|
||||
// querier is used for alerts migrated after the introduction of new query builder
|
||||
querier querier.Querier
|
||||
reader interfaces.Reader
|
||||
|
||||
provider anomaly.Provider
|
||||
// querierV2 is used for alerts created after the introduction of new metrics query builder
|
||||
querierV2 interfaces.Querier
|
||||
|
||||
// querierV5 is used for alerts migrated after the introduction of new query builder
|
||||
querierV5 querierV5.Querier
|
||||
|
||||
provider anomaly.Provider
|
||||
providerV2 anomalyV2.Provider
|
||||
|
||||
version string
|
||||
logger *slog.Logger
|
||||
@@ -46,16 +70,18 @@ func NewAnomalyRule(
|
||||
id string,
|
||||
orgID valuer.UUID,
|
||||
p *ruletypes.PostableRule,
|
||||
querier querier.Querier,
|
||||
reader interfaces.Reader,
|
||||
querierV5 querierV5.Querier,
|
||||
logger *slog.Logger,
|
||||
cache cache.Cache,
|
||||
opts ...baserules.RuleOption,
|
||||
) (*AnomalyRule, error) {
|
||||
|
||||
logger.Info("creating new AnomalyRule", slog.String("rule.id", id))
|
||||
logger.Info("creating new AnomalyRule", "rule_id", id)
|
||||
|
||||
opts = append(opts, baserules.WithLogger(logger))
|
||||
|
||||
baseRule, err := baserules.NewBaseRule(id, orgID, p, opts...)
|
||||
baseRule, err := baserules.NewBaseRule(id, orgID, p, reader, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -75,38 +101,93 @@ func NewAnomalyRule(
|
||||
t.seasonality = anomaly.SeasonalityDaily
|
||||
}
|
||||
|
||||
logger.Info("using seasonality", slog.String("rule.id", id), slog.String("rule.seasonality", t.seasonality.StringValue()))
|
||||
logger.Info("using seasonality", "seasonality", t.seasonality.String())
|
||||
|
||||
querierOptsV2 := querierV2.QuerierOptions{
|
||||
Reader: reader,
|
||||
Cache: cache,
|
||||
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
||||
}
|
||||
|
||||
t.querierV2 = querierV2.NewQuerier(querierOptsV2)
|
||||
t.reader = reader
|
||||
if t.seasonality == anomaly.SeasonalityHourly {
|
||||
t.provider = anomaly.NewHourlyProvider(
|
||||
anomaly.WithQuerier[*anomaly.HourlyProvider](querier),
|
||||
anomaly.WithLogger[*anomaly.HourlyProvider](logger),
|
||||
anomaly.WithCache[*anomaly.HourlyProvider](cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.HourlyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.HourlyProvider](reader),
|
||||
)
|
||||
} else if t.seasonality == anomaly.SeasonalityDaily {
|
||||
t.provider = anomaly.NewDailyProvider(
|
||||
anomaly.WithQuerier[*anomaly.DailyProvider](querier),
|
||||
anomaly.WithLogger[*anomaly.DailyProvider](logger),
|
||||
anomaly.WithCache[*anomaly.DailyProvider](cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.DailyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.DailyProvider](reader),
|
||||
)
|
||||
} else if t.seasonality == anomaly.SeasonalityWeekly {
|
||||
t.provider = anomaly.NewWeeklyProvider(
|
||||
anomaly.WithQuerier[*anomaly.WeeklyProvider](querier),
|
||||
anomaly.WithLogger[*anomaly.WeeklyProvider](logger),
|
||||
anomaly.WithCache[*anomaly.WeeklyProvider](cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.WeeklyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.WeeklyProvider](reader),
|
||||
)
|
||||
}
|
||||
|
||||
t.querier = querier
|
||||
if t.seasonality == anomaly.SeasonalityHourly {
|
||||
t.providerV2 = anomalyV2.NewHourlyProvider(
|
||||
anomalyV2.WithQuerier[*anomalyV2.HourlyProvider](querierV5),
|
||||
anomalyV2.WithLogger[*anomalyV2.HourlyProvider](logger),
|
||||
)
|
||||
} else if t.seasonality == anomaly.SeasonalityDaily {
|
||||
t.providerV2 = anomalyV2.NewDailyProvider(
|
||||
anomalyV2.WithQuerier[*anomalyV2.DailyProvider](querierV5),
|
||||
anomalyV2.WithLogger[*anomalyV2.DailyProvider](logger),
|
||||
)
|
||||
} else if t.seasonality == anomaly.SeasonalityWeekly {
|
||||
t.providerV2 = anomalyV2.NewWeeklyProvider(
|
||||
anomalyV2.WithQuerier[*anomalyV2.WeeklyProvider](querierV5),
|
||||
anomalyV2.WithLogger[*anomalyV2.WeeklyProvider](logger),
|
||||
)
|
||||
}
|
||||
|
||||
t.querierV5 = querierV5
|
||||
t.version = p.Version
|
||||
t.logger = logger
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) Type() ruletypes.RuleType {
|
||||
return ruletypes.RuleTypeAnomaly
|
||||
return RuleTypeAnomaly
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) prepareQueryRange(ctx context.Context, ts time.Time) *qbtypes.QueryRangeRequest {
|
||||
func (r *AnomalyRule) prepareQueryRange(ctx context.Context, ts time.Time) (*v3.QueryRangeParamsV3, error) {
|
||||
|
||||
r.logger.InfoContext(ctx, "prepare query range request", slog.String("rule.id", r.ID()), slog.Int64("ts", ts.UnixMilli()), slog.Int64("eval.window_ms", r.EvalWindow().Milliseconds()), slog.Int64("eval.delay_ms", r.EvalDelay().Milliseconds()))
|
||||
r.logger.InfoContext(
|
||||
ctx, "prepare query range request v4", "ts", ts.UnixMilli(), "eval_window", r.EvalWindow().Milliseconds(), "eval_delay", r.EvalDelay().Milliseconds(),
|
||||
)
|
||||
|
||||
st, en := r.Timestamps(ts)
|
||||
start := st.UnixMilli()
|
||||
end := en.UnixMilli()
|
||||
|
||||
compositeQuery := r.Condition().CompositeQuery
|
||||
|
||||
if compositeQuery.PanelType != v3.PanelTypeGraph {
|
||||
compositeQuery.PanelType = v3.PanelTypeGraph
|
||||
}
|
||||
|
||||
// default mode
|
||||
return &v3.QueryRangeParamsV3{
|
||||
Start: start,
|
||||
End: end,
|
||||
Step: int64(math.Max(float64(common.MinAllowedStepInterval(start, end)), 60)),
|
||||
CompositeQuery: compositeQuery,
|
||||
Variables: make(map[string]interface{}, 0),
|
||||
NoCache: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) prepareQueryRangeV5(ctx context.Context, ts time.Time) (*qbtypes.QueryRangeRequest, error) {
|
||||
|
||||
r.logger.InfoContext(ctx, "prepare query range request v5", "ts", ts.UnixMilli(), "eval_window", r.EvalWindow().Milliseconds(), "eval_delay", r.EvalDelay().Milliseconds())
|
||||
|
||||
startTs, endTs := r.Timestamps(ts)
|
||||
start, end := startTs.UnixMilli(), endTs.UnixMilli()
|
||||
@@ -122,14 +203,25 @@ func (r *AnomalyRule) prepareQueryRange(ctx context.Context, ts time.Time) *qbty
|
||||
}
|
||||
req.CompositeQuery.Queries = make([]qbtypes.QueryEnvelope, len(r.Condition().CompositeQuery.Queries))
|
||||
copy(req.CompositeQuery.Queries, r.Condition().CompositeQuery.Queries)
|
||||
return req
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) GetSelectedQuery() string {
|
||||
return r.Condition().GetSelectedQueryName()
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, ts time.Time) (ruletypes.Vector, error) {
|
||||
|
||||
params := r.prepareQueryRange(ctx, ts)
|
||||
params, err := r.prepareQueryRange(ctx, ts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = r.PopulateTemporality(ctx, orgID, params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("internal error while setting temporality")
|
||||
}
|
||||
|
||||
anomalies, err := r.provider.GetAnomalies(ctx, orgID, &anomaly.AnomaliesRequest{
|
||||
anomalies, err := r.provider.GetAnomalies(ctx, orgID, &anomaly.GetAnomaliesRequest{
|
||||
Params: params,
|
||||
Seasonality: r.seasonality,
|
||||
})
|
||||
@@ -137,43 +229,87 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var queryResult *qbtypes.TimeSeriesData
|
||||
var queryResult *v3.Result
|
||||
for _, result := range anomalies.Results {
|
||||
if result.QueryName == r.SelectedQuery(ctx) {
|
||||
if result.QueryName == r.GetSelectedQuery() {
|
||||
queryResult = result
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if queryResult == nil {
|
||||
r.logger.WarnContext(ctx, "nil qb result", slog.String("rule.id", r.ID()), slog.Int64("ts", ts.UnixMilli()))
|
||||
return ruletypes.Vector{}, nil
|
||||
}
|
||||
|
||||
hasData := len(queryResult.Aggregations) > 0 &&
|
||||
queryResult.Aggregations[0] != nil &&
|
||||
len(queryResult.Aggregations[0].AnomalyScores) > 0
|
||||
|
||||
hasData := len(queryResult.AnomalyScores) > 0
|
||||
if missingDataAlert := r.HandleMissingDataAlert(ctx, ts, hasData); missingDataAlert != nil {
|
||||
return ruletypes.Vector{*missingDataAlert}, nil
|
||||
} else if !hasData {
|
||||
r.logger.WarnContext(ctx, "no anomaly result", slog.String("rule.id", r.ID()))
|
||||
return ruletypes.Vector{}, nil
|
||||
}
|
||||
|
||||
var resultVector ruletypes.Vector
|
||||
|
||||
scoresJSON, _ := json.Marshal(queryResult.Aggregations[0].AnomalyScores)
|
||||
// TODO(srikanthccv): this could be noisy but we do this to answer false alert requests
|
||||
r.logger.InfoContext(ctx, "anomaly scores", slog.String("rule.id", r.ID()), slog.String("anomaly.scores", string(scoresJSON)))
|
||||
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
|
||||
r.logger.InfoContext(ctx, "anomaly scores", "scores", string(scoresJSON))
|
||||
|
||||
for _, series := range queryResult.AnomalyScores {
|
||||
if !r.Condition().ShouldEval(series) {
|
||||
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
|
||||
continue
|
||||
}
|
||||
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
|
||||
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
||||
SendUnmatched: r.ShouldSendUnmatched(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resultVector = append(resultVector, results...)
|
||||
}
|
||||
return resultVector, nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID, ts time.Time) (ruletypes.Vector, error) {
|
||||
|
||||
params, err := r.prepareQueryRangeV5(ctx, ts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
anomalies, err := r.providerV2.GetAnomalies(ctx, orgID, &anomalyV2.AnomaliesRequest{
|
||||
Params: *params,
|
||||
Seasonality: anomalyV2.Seasonality{String: valuer.NewString(r.seasonality.String())},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var qbResult *qbtypes.TimeSeriesData
|
||||
for _, result := range anomalies.Results {
|
||||
if result.QueryName == r.GetSelectedQuery() {
|
||||
qbResult = result
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if qbResult == nil {
|
||||
r.logger.WarnContext(ctx, "nil qb result", "ts", ts.UnixMilli())
|
||||
}
|
||||
|
||||
queryResult := transition.ConvertV5TimeSeriesDataToV4Result(qbResult)
|
||||
|
||||
hasData := len(queryResult.AnomalyScores) > 0
|
||||
if missingDataAlert := r.HandleMissingDataAlert(ctx, ts, hasData); missingDataAlert != nil {
|
||||
return ruletypes.Vector{*missingDataAlert}, nil
|
||||
}
|
||||
|
||||
var resultVector ruletypes.Vector
|
||||
|
||||
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
|
||||
r.logger.InfoContext(ctx, "anomaly scores", "scores", string(scoresJSON))
|
||||
|
||||
// Filter out new series if newGroupEvalDelay is configured
|
||||
seriesToProcess := queryResult.Aggregations[0].AnomalyScores
|
||||
seriesToProcess := queryResult.AnomalyScores
|
||||
if r.ShouldSkipNewGroups() {
|
||||
filteredSeries, filterErr := r.BaseRule.FilterNewSeries(ctx, ts, seriesToProcess)
|
||||
// In case of error we log the error and continue with the original series
|
||||
if filterErr != nil {
|
||||
r.logger.ErrorContext(ctx, "error filtering new series", slog.String("rule.id", r.ID()), errors.Attr(filterErr))
|
||||
r.logger.ErrorContext(ctx, "Error filtering new series, ", errors.Attr(filterErr), "rule_name", r.Name())
|
||||
} else {
|
||||
seriesToProcess = filteredSeries
|
||||
}
|
||||
@@ -181,10 +317,10 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t
|
||||
|
||||
for _, series := range seriesToProcess {
|
||||
if !r.Condition().ShouldEval(series) {
|
||||
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", slog.String("rule.id", r.ID()), slog.Int("series.num_points", len(series.Values)), slog.Int("series.required_points", r.Condition().RequiredNumPoints))
|
||||
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
|
||||
continue
|
||||
}
|
||||
results, err := r.Threshold.Eval(series, r.Unit(), ruletypes.EvalData{
|
||||
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
|
||||
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
||||
SendUnmatched: r.ShouldSendUnmatched(),
|
||||
})
|
||||
@@ -205,9 +341,13 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
var res ruletypes.Vector
|
||||
var err error
|
||||
|
||||
r.logger.InfoContext(ctx, "running query", slog.String("rule.id", r.ID()))
|
||||
res, err = r.buildAndRunQuery(ctx, r.OrgID(), ts)
|
||||
|
||||
if r.version == "v5" {
|
||||
r.logger.InfoContext(ctx, "running v5 query")
|
||||
res, err = r.buildAndRunQueryV5(ctx, r.OrgID(), ts)
|
||||
} else {
|
||||
r.logger.InfoContext(ctx, "running v4 query")
|
||||
res, err = r.buildAndRunQuery(ctx, r.OrgID(), ts)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -231,7 +371,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
}
|
||||
value := valueFormatter.Format(smpl.V, r.Unit())
|
||||
threshold := valueFormatter.Format(smpl.Target, smpl.TargetUnit)
|
||||
r.logger.DebugContext(ctx, "alert template data for rule", slog.String("rule.id", r.ID()), slog.String("formatter.name", valueFormatter.Name()), slog.String("alert.value", value), slog.String("alert.threshold", threshold))
|
||||
r.logger.DebugContext(ctx, "Alert template data for rule", "rule_name", r.Name(), "formatter", valueFormatter.Name(), "value", value, "threshold", threshold)
|
||||
|
||||
tmplData := ruletypes.AlertTemplateData(l, value, threshold)
|
||||
// Inject some convenience variables that are easier to remember for users
|
||||
@@ -246,34 +386,35 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
defs+text,
|
||||
"__alert_"+r.Name(),
|
||||
tmplData,
|
||||
times.Time(timestamp.FromTime(ts)),
|
||||
nil,
|
||||
)
|
||||
result, err := tmpl.Expand()
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("<error expanding template: %s>", err)
|
||||
r.logger.ErrorContext(ctx, "expanding alert template failed", slog.String("rule.id", r.ID()), errors.Attr(err), slog.Any("alert.template_data", tmplData))
|
||||
r.logger.ErrorContext(ctx, "Expanding alert template failed", errors.Attr(err), "data", tmplData, "rule_name", r.Name())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
lb := ruletypes.NewBuilder(smpl.Metric...).Del(ruletypes.MetricNameLabel).Del(ruletypes.TemporalityLabel)
|
||||
resultLabels := ruletypes.NewBuilder(smpl.Metric...).Del(ruletypes.MetricNameLabel).Del(ruletypes.TemporalityLabel).Labels()
|
||||
lb := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel)
|
||||
resultLabels := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel).Labels()
|
||||
|
||||
for name, value := range r.Labels().Map() {
|
||||
lb.Set(name, expand(value))
|
||||
}
|
||||
|
||||
lb.Set(ruletypes.AlertNameLabel, r.Name())
|
||||
lb.Set(ruletypes.AlertRuleIDLabel, r.ID())
|
||||
lb.Set(ruletypes.RuleSourceLabel, r.GeneratorURL())
|
||||
lb.Set(labels.AlertNameLabel, r.Name())
|
||||
lb.Set(labels.AlertRuleIdLabel, r.ID())
|
||||
lb.Set(labels.RuleSourceLabel, r.GeneratorURL())
|
||||
|
||||
annotations := make(ruletypes.Labels, 0, len(r.Annotations().Map()))
|
||||
annotations := make(labels.Labels, 0, len(r.Annotations().Map()))
|
||||
for name, value := range r.Annotations().Map() {
|
||||
annotations = append(annotations, ruletypes.Label{Name: name, Value: expand(value)})
|
||||
annotations = append(annotations, labels.Label{Name: name, Value: expand(value)})
|
||||
}
|
||||
if smpl.IsMissing {
|
||||
lb.Set(ruletypes.AlertNameLabel, "[No data] "+r.Name())
|
||||
lb.Set(ruletypes.NoDataLabel, "true")
|
||||
lb.Set(labels.AlertNameLabel, "[No data] "+r.Name())
|
||||
lb.Set(labels.NoDataLabel, "true")
|
||||
}
|
||||
|
||||
lbs := lb.Labels()
|
||||
@@ -281,17 +422,17 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
resultFPs[h] = struct{}{}
|
||||
|
||||
if _, ok := alerts[h]; ok {
|
||||
r.logger.ErrorContext(ctx, "the alert query returns duplicate records", slog.String("rule.id", r.ID()), slog.Any("alert", alerts[h]))
|
||||
err = errors.NewInternalf(errors.CodeInternal, "duplicate alert found, vector contains metrics with the same labelset after applying alert labels")
|
||||
r.logger.ErrorContext(ctx, "the alert query returns duplicate records", "rule_id", r.ID(), "alert", alerts[h])
|
||||
err = fmt.Errorf("duplicate alert found, vector contains metrics with the same labelset after applying alert labels")
|
||||
return 0, err
|
||||
}
|
||||
|
||||
alerts[h] = &ruletypes.Alert{
|
||||
Labels: lbs,
|
||||
QueryResultLabels: resultLabels,
|
||||
QueryResultLables: resultLabels,
|
||||
Annotations: annotations,
|
||||
ActiveAt: ts,
|
||||
State: ruletypes.StatePending,
|
||||
State: model.StatePending,
|
||||
Value: smpl.V,
|
||||
GeneratorURL: r.GeneratorURL(),
|
||||
Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]],
|
||||
@@ -300,12 +441,12 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
r.logger.InfoContext(ctx, "number of alerts found", slog.String("rule.id", r.ID()), slog.Int("alert.count", len(alerts)))
|
||||
r.logger.InfoContext(ctx, "number of alerts found", "rule_name", r.Name(), "alerts_count", len(alerts))
|
||||
// alerts[h] is ready, add or update active list now
|
||||
for h, a := range alerts {
|
||||
// Check whether we already have alerting state for the identifying label set.
|
||||
// Update the last value and annotations if so, create a new alert entry otherwise.
|
||||
if alert, ok := r.Active[h]; ok && alert.State != ruletypes.StateInactive {
|
||||
if alert, ok := r.Active[h]; ok && alert.State != model.StateInactive {
|
||||
|
||||
alert.Value = a.Value
|
||||
alert.Annotations = a.Annotations
|
||||
@@ -321,76 +462,76 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
r.Active[h] = a
|
||||
}
|
||||
|
||||
itemsToAdd := []rulestatehistorytypes.RuleStateHistory{}
|
||||
itemsToAdd := []model.RuleStateHistory{}
|
||||
|
||||
// Check if any pending alerts should be removed or fire now. Write out alert timeseries.
|
||||
for fp, a := range r.Active {
|
||||
labelsJSON, err := json.Marshal(a.QueryResultLabels)
|
||||
labelsJSON, err := json.Marshal(a.QueryResultLables)
|
||||
if err != nil {
|
||||
r.logger.ErrorContext(ctx, "error marshaling labels", slog.String("rule.id", r.ID()), errors.Attr(err), slog.Any("alert.labels", a.Labels))
|
||||
r.logger.ErrorContext(ctx, "error marshaling labels", errors.Attr(err), "labels", a.Labels)
|
||||
}
|
||||
if _, ok := resultFPs[fp]; !ok {
|
||||
// If the alert was previously firing, keep it around for a given
|
||||
// retention time so it is reported as resolved to the AlertManager.
|
||||
if a.State == ruletypes.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > ruletypes.ResolvedRetention) {
|
||||
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > ruletypes.ResolvedRetention) {
|
||||
delete(r.Active, fp)
|
||||
}
|
||||
if a.State != ruletypes.StateInactive {
|
||||
a.State = ruletypes.StateInactive
|
||||
if a.State != model.StateInactive {
|
||||
a.State = model.StateInactive
|
||||
a.ResolvedAt = ts
|
||||
itemsToAdd = append(itemsToAdd, rulestatehistorytypes.RuleStateHistory{
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: ruletypes.StateInactive,
|
||||
State: model.StateInactive,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: rulestatehistorytypes.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLabels.Hash(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if a.State == ruletypes.StatePending && ts.Sub(a.ActiveAt) >= r.HoldDuration().Duration() {
|
||||
a.State = ruletypes.StateFiring
|
||||
if a.State == model.StatePending && ts.Sub(a.ActiveAt) >= r.HoldDuration().Duration() {
|
||||
a.State = model.StateFiring
|
||||
a.FiredAt = ts
|
||||
state := ruletypes.StateFiring
|
||||
state := model.StateFiring
|
||||
if a.Missing {
|
||||
state = ruletypes.StateNoData
|
||||
state = model.StateNoData
|
||||
}
|
||||
itemsToAdd = append(itemsToAdd, rulestatehistorytypes.RuleStateHistory{
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: state,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: rulestatehistorytypes.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLabels.Hash(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
|
||||
// We need to change firing alert to recovering if the returned sample meets recovery threshold
|
||||
changeFiringToRecovering := a.State == ruletypes.StateFiring && a.IsRecovering
|
||||
changeFiringToRecovering := a.State == model.StateFiring && a.IsRecovering
|
||||
// We need to change recovering alerts to firing if the returned sample meets target threshold
|
||||
changeRecoveringToFiring := a.State == ruletypes.StateRecovering && !a.IsRecovering && !a.Missing
|
||||
changeRecoveringToFiring := a.State == model.StateRecovering && !a.IsRecovering && !a.Missing
|
||||
// in any of the above case we need to update the status of alert
|
||||
if changeFiringToRecovering || changeRecoveringToFiring {
|
||||
state := ruletypes.StateRecovering
|
||||
state := model.StateRecovering
|
||||
if changeRecoveringToFiring {
|
||||
state = ruletypes.StateFiring
|
||||
state = model.StateFiring
|
||||
}
|
||||
a.State = state
|
||||
r.logger.DebugContext(ctx, "converting alert state", slog.String("rule.id", r.ID()), slog.Any("alert.state", state))
|
||||
itemsToAdd = append(itemsToAdd, rulestatehistorytypes.RuleStateHistory{
|
||||
r.logger.DebugContext(ctx, "converting alert state", "name", r.Name(), "state", state)
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: state,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: rulestatehistorytypes.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLabels.Hash(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,19 +2,21 @@ package rules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/anomaly"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/anomaly"
|
||||
)
|
||||
|
||||
// mockAnomalyProvider is a mock implementation of anomaly.Provider for testing.
|
||||
@@ -22,13 +24,13 @@ import (
|
||||
// time periods (current, past period, current season, past season, past 2 seasons,
|
||||
// past 3 seasons), making it cumbersome to create mock data.
|
||||
type mockAnomalyProvider struct {
|
||||
responses []*anomaly.AnomaliesResponse
|
||||
responses []*anomaly.GetAnomaliesResponse
|
||||
callCount int
|
||||
}
|
||||
|
||||
func (m *mockAnomalyProvider) GetAnomalies(ctx context.Context, orgID valuer.UUID, req *anomaly.AnomaliesRequest) (*anomaly.AnomaliesResponse, error) {
|
||||
func (m *mockAnomalyProvider) GetAnomalies(ctx context.Context, orgID valuer.UUID, req *anomaly.GetAnomaliesRequest) (*anomaly.GetAnomaliesResponse, error) {
|
||||
if m.callCount >= len(m.responses) {
|
||||
return &anomaly.AnomaliesResponse{Results: []*qbtypes.TimeSeriesData{}}, nil
|
||||
return &anomaly.GetAnomaliesResponse{Results: []*v3.Result{}}, nil
|
||||
}
|
||||
resp := m.responses[m.callCount]
|
||||
m.callCount++
|
||||
@@ -47,46 +49,45 @@ func TestAnomalyRule_NoData_AlertOnAbsent(t *testing.T) {
|
||||
postableRule := ruletypes.PostableRule{
|
||||
AlertName: "Test anomaly no data",
|
||||
AlertType: ruletypes.AlertTypeMetric,
|
||||
RuleType: ruletypes.RuleTypeAnomaly,
|
||||
RuleType: RuleTypeAnomaly,
|
||||
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
|
||||
EvalWindow: evalWindow,
|
||||
Frequency: valuer.MustParseTextDuration("1m"),
|
||||
}},
|
||||
RuleCondition: &ruletypes.RuleCondition{
|
||||
CompareOperator: ruletypes.ValueIsAbove,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
Target: &target,
|
||||
CompositeQuery: &ruletypes.AlertCompositeQuery{
|
||||
QueryType: ruletypes.QueryTypeBuilder,
|
||||
Queries: []qbtypes.QueryEnvelope{{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
Target: &target,
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"A": {
|
||||
QueryName: "A",
|
||||
Expression: "A",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
Temporality: v3.Unspecified,
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
SelectedQuery: "A",
|
||||
Seasonality: "daily",
|
||||
Thresholds: &ruletypes.RuleThresholdData{
|
||||
Kind: ruletypes.BasicThresholdKind,
|
||||
Spec: ruletypes.BasicRuleThresholds{{
|
||||
Name: "Test anomaly no data",
|
||||
TargetValue: &target,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
CompareOperator: ruletypes.ValueIsAbove,
|
||||
Name: "Test anomaly no data",
|
||||
TargetValue: &target,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
responseNoData := &anomaly.AnomaliesResponse{
|
||||
Results: []*qbtypes.TimeSeriesData{
|
||||
responseNoData := &anomaly.GetAnomaliesResponse{
|
||||
Results: []*v3.Result{
|
||||
{
|
||||
QueryName: "A",
|
||||
Aggregations: []*qbtypes.AggregationBucket{{
|
||||
AnomalyScores: []*qbtypes.TimeSeries{},
|
||||
}},
|
||||
QueryName: "A",
|
||||
AnomalyScores: []*v3.Series{},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -114,17 +115,23 @@ func TestAnomalyRule_NoData_AlertOnAbsent(t *testing.T) {
|
||||
t.Run(c.description, func(t *testing.T) {
|
||||
postableRule.RuleCondition.AlertOnAbsent = c.alertOnAbsent
|
||||
|
||||
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
|
||||
options := clickhouseReader.NewOptions("primaryNamespace")
|
||||
reader := clickhouseReader.NewReader(slog.Default(), nil, telemetryStore, nil, "", time.Second, nil, nil, options)
|
||||
|
||||
rule, err := NewAnomalyRule(
|
||||
"test-anomaly-rule",
|
||||
valuer.GenerateUUID(),
|
||||
&postableRule,
|
||||
reader,
|
||||
nil,
|
||||
logger,
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
rule.provider = &mockAnomalyProvider{
|
||||
responses: []*anomaly.AnomaliesResponse{responseNoData},
|
||||
responses: []*anomaly.GetAnomaliesResponse{responseNoData},
|
||||
}
|
||||
|
||||
alertsFound, err := rule.Eval(context.Background(), evalTime)
|
||||
@@ -149,47 +156,46 @@ func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
|
||||
postableRule := ruletypes.PostableRule{
|
||||
AlertName: "Test anomaly no data with AbsentFor",
|
||||
AlertType: ruletypes.AlertTypeMetric,
|
||||
RuleType: ruletypes.RuleTypeAnomaly,
|
||||
RuleType: RuleTypeAnomaly,
|
||||
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
|
||||
EvalWindow: evalWindow,
|
||||
Frequency: valuer.MustParseTextDuration("1m"),
|
||||
}},
|
||||
RuleCondition: &ruletypes.RuleCondition{
|
||||
CompareOperator: ruletypes.ValueIsAbove,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
AlertOnAbsent: true,
|
||||
Target: &target,
|
||||
CompositeQuery: &ruletypes.AlertCompositeQuery{
|
||||
QueryType: ruletypes.QueryTypeBuilder,
|
||||
Queries: []qbtypes.QueryEnvelope{{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
AlertOnAbsent: true,
|
||||
Target: &target,
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"A": {
|
||||
QueryName: "A",
|
||||
Expression: "A",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
Temporality: v3.Unspecified,
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
SelectedQuery: "A",
|
||||
Seasonality: "daily",
|
||||
Thresholds: &ruletypes.RuleThresholdData{
|
||||
Kind: ruletypes.BasicThresholdKind,
|
||||
Spec: ruletypes.BasicRuleThresholds{{
|
||||
Name: "Test anomaly no data with AbsentFor",
|
||||
TargetValue: &target,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
CompareOperator: ruletypes.ValueIsAbove,
|
||||
Name: "Test anomaly no data with AbsentFor",
|
||||
TargetValue: &target,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
responseNoData := &anomaly.AnomaliesResponse{
|
||||
Results: []*qbtypes.TimeSeriesData{
|
||||
responseNoData := &anomaly.GetAnomaliesResponse{
|
||||
Results: []*v3.Result{
|
||||
{
|
||||
QueryName: "A",
|
||||
Aggregations: []*qbtypes.AggregationBucket{{
|
||||
AnomalyScores: []*qbtypes.TimeSeries{},
|
||||
}},
|
||||
QueryName: "A",
|
||||
AnomalyScores: []*v3.Series{},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -223,35 +229,32 @@ func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
|
||||
t1 := baseTime.Add(5 * time.Minute)
|
||||
t2 := t1.Add(c.timeBetweenEvals)
|
||||
|
||||
responseWithData := &anomaly.AnomaliesResponse{
|
||||
Results: []*qbtypes.TimeSeriesData{
|
||||
responseWithData := &anomaly.GetAnomaliesResponse{
|
||||
Results: []*v3.Result{
|
||||
{
|
||||
QueryName: "A",
|
||||
Aggregations: []*qbtypes.AggregationBucket{{
|
||||
AnomalyScores: []*qbtypes.TimeSeries{
|
||||
{
|
||||
Labels: []*qbtypes.Label{
|
||||
{
|
||||
Key: telemetrytypes.TelemetryFieldKey{Name: "Test"},
|
||||
Value: "labels",
|
||||
},
|
||||
},
|
||||
Values: []*qbtypes.TimeSeriesValue{
|
||||
{Timestamp: baseTime.UnixMilli(), Value: 1.0},
|
||||
{Timestamp: baseTime.Add(time.Minute).UnixMilli(), Value: 1.5},
|
||||
},
|
||||
AnomalyScores: []*v3.Series{
|
||||
{
|
||||
Labels: map[string]string{"test": "label"},
|
||||
Points: []v3.Point{
|
||||
{Timestamp: baseTime.UnixMilli(), Value: 1.0},
|
||||
{Timestamp: baseTime.Add(time.Minute).UnixMilli(), Value: 1.5},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, nil, logger)
|
||||
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
|
||||
options := clickhouseReader.NewOptions("primaryNamespace")
|
||||
reader := clickhouseReader.NewReader(slog.Default(), nil, telemetryStore, nil, "", time.Second, nil, nil, options)
|
||||
|
||||
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, reader, nil, logger, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
rule.provider = &mockAnomalyProvider{
|
||||
responses: []*anomaly.AnomaliesResponse{responseWithData, responseNoData},
|
||||
responses: []*anomaly.GetAnomaliesResponse{responseWithData, responseNoData},
|
||||
}
|
||||
|
||||
alertsFound1, err := rule.Eval(context.Background(), t1)
|
||||
|
||||
@@ -11,7 +11,9 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
@@ -21,7 +23,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
rules := make([]baserules.Rule, 0)
|
||||
var task baserules.Task
|
||||
|
||||
ruleID := baserules.RuleIDFromTaskName(opts.TaskName)
|
||||
ruleId := baserules.RuleIdFromTaskName(opts.TaskName)
|
||||
evaluation, err := opts.Rule.Evaluation.GetEvaluation()
|
||||
if err != nil {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err)
|
||||
@@ -30,9 +32,10 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
|
||||
// create a threshold rule
|
||||
tr, err := baserules.NewThresholdRule(
|
||||
ruleID,
|
||||
ruleId,
|
||||
opts.OrgID,
|
||||
opts.Rule,
|
||||
opts.Reader,
|
||||
opts.Querier,
|
||||
opts.Logger,
|
||||
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
|
||||
@@ -55,10 +58,11 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
|
||||
// create promql rule
|
||||
pr, err := baserules.NewPromRule(
|
||||
ruleID,
|
||||
ruleId,
|
||||
opts.OrgID,
|
||||
opts.Rule,
|
||||
opts.Logger,
|
||||
opts.Reader,
|
||||
opts.ManagerOpts.Prometheus,
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
|
||||
@@ -78,11 +82,13 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeAnomaly {
|
||||
// create anomaly rule
|
||||
ar, err := NewAnomalyRule(
|
||||
ruleID,
|
||||
ruleId,
|
||||
opts.OrgID,
|
||||
opts.Rule,
|
||||
opts.Reader,
|
||||
opts.Querier,
|
||||
opts.Logger,
|
||||
opts.Cache,
|
||||
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
|
||||
@@ -99,7 +105,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
|
||||
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
|
||||
}
|
||||
|
||||
return task, nil
|
||||
@@ -107,12 +113,12 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
|
||||
// TestNotification prepares a dummy rule for given rule parameters and
|
||||
// sends a test notification. returns alert count and error (if any)
|
||||
func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
|
||||
func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.ApiError) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if opts.Rule == nil {
|
||||
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "rule is required")
|
||||
return 0, basemodel.BadRequest(fmt.Errorf("rule is required"))
|
||||
}
|
||||
|
||||
parsedRule := opts.Rule
|
||||
@@ -132,14 +138,15 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
|
||||
if parsedRule.RuleType == ruletypes.RuleTypeThreshold {
|
||||
|
||||
// add special labels for test alerts
|
||||
parsedRule.Labels[ruletypes.RuleSourceLabel] = ""
|
||||
parsedRule.Labels[ruletypes.AlertRuleIDLabel] = ""
|
||||
parsedRule.Labels[labels.RuleSourceLabel] = ""
|
||||
parsedRule.Labels[labels.AlertRuleIdLabel] = ""
|
||||
|
||||
// create a threshold rule
|
||||
rule, err = baserules.NewThresholdRule(
|
||||
alertname,
|
||||
opts.OrgID,
|
||||
parsedRule,
|
||||
opts.Reader,
|
||||
opts.Querier,
|
||||
opts.Logger,
|
||||
baserules.WithSendAlways(),
|
||||
@@ -151,7 +158,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
|
||||
|
||||
if err != nil {
|
||||
slog.Error("failed to prepare a new threshold rule for test", "name", alertname, errors.Attr(err))
|
||||
return 0, err
|
||||
return 0, basemodel.BadRequest(err)
|
||||
}
|
||||
|
||||
} else if parsedRule.RuleType == ruletypes.RuleTypeProm {
|
||||
@@ -162,6 +169,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
|
||||
opts.OrgID,
|
||||
parsedRule,
|
||||
opts.Logger,
|
||||
opts.Reader,
|
||||
opts.ManagerOpts.Prometheus,
|
||||
baserules.WithSendAlways(),
|
||||
baserules.WithSendUnmatched(),
|
||||
@@ -172,7 +180,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
|
||||
|
||||
if err != nil {
|
||||
slog.Error("failed to prepare a new promql rule for test", "name", alertname, errors.Attr(err))
|
||||
return 0, err
|
||||
return 0, basemodel.BadRequest(err)
|
||||
}
|
||||
} else if parsedRule.RuleType == ruletypes.RuleTypeAnomaly {
|
||||
// create anomaly rule
|
||||
@@ -180,8 +188,10 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
|
||||
alertname,
|
||||
opts.OrgID,
|
||||
parsedRule,
|
||||
opts.Reader,
|
||||
opts.Querier,
|
||||
opts.Logger,
|
||||
opts.Cache,
|
||||
baserules.WithSendAlways(),
|
||||
baserules.WithSendUnmatched(),
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
@@ -190,10 +200,10 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("failed to prepare a new anomaly rule for test", "name", alertname, errors.Attr(err))
|
||||
return 0, err
|
||||
return 0, basemodel.BadRequest(err)
|
||||
}
|
||||
} else {
|
||||
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to derive ruletype with given information")
|
||||
return 0, basemodel.BadRequest(fmt.Errorf("failed to derive ruletype with given information"))
|
||||
}
|
||||
|
||||
// set timestamp to current utc time
|
||||
@@ -202,7 +212,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
|
||||
alertsFound, err := rule.Eval(ctx, ts)
|
||||
if err != nil {
|
||||
slog.Error("evaluating rule failed", "rule", rule.Name(), errors.Attr(err))
|
||||
return 0, err
|
||||
return 0, basemodel.InternalError(fmt.Errorf("rule evaluation failed"))
|
||||
}
|
||||
rule.SendAlerts(ctx, ts, 0, time.Minute, opts.NotifyFunc)
|
||||
|
||||
|
||||
@@ -114,8 +114,11 @@ func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
count, err := mgr.TestNotification(context.Background(), orgID, string(ruleBytes))
|
||||
require.Nil(t, err)
|
||||
count, apiErr := mgr.TestNotification(context.Background(), orgID, string(ruleBytes))
|
||||
if apiErr != nil {
|
||||
t.Logf("TestNotification error: %v, type: %s", apiErr.Err, apiErr.Typ)
|
||||
}
|
||||
require.Nil(t, apiErr)
|
||||
assert.Equal(t, tc.ExpectAlerts, count)
|
||||
|
||||
if tc.ExpectAlerts > 0 {
|
||||
@@ -265,8 +268,11 @@ func TestManager_TestNotification_SendUnmatched_PromRule(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
count, err := mgr.TestNotification(context.Background(), orgID, string(ruleBytes))
|
||||
require.Nil(t, err)
|
||||
count, apiErr := mgr.TestNotification(context.Background(), orgID, string(ruleBytes))
|
||||
if apiErr != nil {
|
||||
t.Logf("TestNotification error: %v, type: %s", apiErr.Err, apiErr.Typ)
|
||||
}
|
||||
require.Nil(t, apiErr)
|
||||
assert.Equal(t, tc.ExpectAlerts, count)
|
||||
|
||||
if tc.ExpectAlerts > 0 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -628,6 +628,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
|
||||
@@ -941,101 +1038,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);
|
||||
};
|
||||
|
||||
@@ -550,12 +550,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 +588,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 +600,7 @@ export interface CloudintegrationtypesAWSLogsStrategyDTO {
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
cloudwatch_logs_subscriptions?:
|
||||
cloudwatchLogsSubscriptions?:
|
||||
| CloudintegrationtypesAWSLogsStrategyDTOCloudwatchLogsSubscriptionsItem[]
|
||||
| null;
|
||||
}
|
||||
@@ -621,7 +621,7 @@ export interface CloudintegrationtypesAWSMetricsStrategyDTO {
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
cloudwatch_metric_stream_filters?:
|
||||
cloudwatchMetricStreamFilters?:
|
||||
| CloudintegrationtypesAWSMetricsStrategyDTOCloudwatchMetricStreamFiltersItem[]
|
||||
| null;
|
||||
}
|
||||
@@ -726,6 +726,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
|
||||
@@ -864,9 +890,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 +989,7 @@ export interface CloudintegrationtypesProviderIntegrationConfigDTO {
|
||||
|
||||
export interface CloudintegrationtypesServiceDTO {
|
||||
assets: CloudintegrationtypesAssetsDTO;
|
||||
cloudIntegrationService: CloudintegrationtypesCloudIntegrationServiceDTO;
|
||||
dataCollected: CloudintegrationtypesDataCollectedDTO;
|
||||
/**
|
||||
* @type string
|
||||
@@ -917,8 +1003,7 @@ export interface CloudintegrationtypesServiceDTO {
|
||||
* @type string
|
||||
*/
|
||||
overview: string;
|
||||
serviceConfig?: CloudintegrationtypesServiceConfigDTO;
|
||||
supported_signals: CloudintegrationtypesSupportedSignalsDTO;
|
||||
supportedSignals: CloudintegrationtypesSupportedSignalsDTO;
|
||||
telemetryCollectionStrategy: CloudintegrationtypesCollectionStrategyDTO;
|
||||
/**
|
||||
* @type string
|
||||
@@ -930,6 +1015,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
|
||||
@@ -2710,6 +2810,14 @@ export interface RenderErrorResponseDTO {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export enum RulestatehistorytypesAlertStateDTO {
|
||||
inactive = 'inactive',
|
||||
pending = 'pending',
|
||||
recovering = 'recovering',
|
||||
firing = 'firing',
|
||||
nodata = 'nodata',
|
||||
disabled = 'disabled',
|
||||
}
|
||||
export interface RulestatehistorytypesGettableRuleStateHistoryDTO {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -2721,7 +2829,7 @@ export interface RulestatehistorytypesGettableRuleStateHistoryDTO {
|
||||
* @nullable true
|
||||
*/
|
||||
labels: Querybuildertypesv5LabelDTO[] | null;
|
||||
overallState: RuletypesAlertStateDTO;
|
||||
overallState: RulestatehistorytypesAlertStateDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -2729,12 +2837,12 @@ export interface RulestatehistorytypesGettableRuleStateHistoryDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
ruleId: string;
|
||||
ruleID: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
ruleName: string;
|
||||
state: RuletypesAlertStateDTO;
|
||||
state: RulestatehistorytypesAlertStateDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -2832,17 +2940,9 @@ export interface RulestatehistorytypesGettableRuleStateWindowDTO {
|
||||
* @format int64
|
||||
*/
|
||||
start: number;
|
||||
state: RuletypesAlertStateDTO;
|
||||
state: RulestatehistorytypesAlertStateDTO;
|
||||
}
|
||||
|
||||
export enum RuletypesAlertStateDTO {
|
||||
inactive = 'inactive',
|
||||
pending = 'pending',
|
||||
recovering = 'recovering',
|
||||
firing = 'firing',
|
||||
nodata = 'nodata',
|
||||
disabled = 'disabled',
|
||||
}
|
||||
export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -3532,6 +3632,11 @@ export type UpdateAccountPathParameters = {
|
||||
cloudProvider: string;
|
||||
id: string;
|
||||
};
|
||||
export type UpdateServicePathParameters = {
|
||||
cloudProvider: string;
|
||||
id: string;
|
||||
serviceId: string;
|
||||
};
|
||||
export type AgentCheckInPathParameters = {
|
||||
cloudProvider: string;
|
||||
};
|
||||
@@ -3566,10 +3671,6 @@ export type GetService200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateServicePathParameters = {
|
||||
cloudProvider: string;
|
||||
serviceId: string;
|
||||
};
|
||||
export type CreateSessionByGoogleCallback303 = {
|
||||
data: AuthtypesGettableTokenDTO;
|
||||
/**
|
||||
@@ -4613,7 +4714,7 @@ export type GetRuleHistoryTimelineParams = {
|
||||
/**
|
||||
* @description undefined
|
||||
*/
|
||||
state?: RuletypesAlertStateDTO;
|
||||
state?: RulestatehistorytypesAlertStateDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
|
||||
@@ -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>),
|
||||
@@ -0,0 +1,97 @@
|
||||
.announcement-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--padding-2) var(--padding-4);
|
||||
height: 40px;
|
||||
font-family: var(--font-sans), sans-serif;
|
||||
font-size: var(--label-base-500-font-size);
|
||||
line-height: var(--label-base-500-line-height);
|
||||
font-weight: var(--label-base-500-font-weight);
|
||||
letter-spacing: -0.065px;
|
||||
|
||||
&--warning {
|
||||
background-color: var(--callout-warning-background);
|
||||
color: var(--callout-warning-description);
|
||||
.announcement-banner__action,
|
||||
.announcement-banner__dismiss {
|
||||
background: var(--callout-warning-border);
|
||||
}
|
||||
}
|
||||
|
||||
&--info {
|
||||
background-color: var(--callout-primary-background);
|
||||
color: var(--callout-primary-description);
|
||||
.announcement-banner__action,
|
||||
.announcement-banner__dismiss {
|
||||
background: var(--callout-primary-border);
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
background-color: var(--callout-error-background);
|
||||
color: var(--callout-error-description);
|
||||
.announcement-banner__action,
|
||||
.announcement-banner__dismiss {
|
||||
background: var(--callout-error-border);
|
||||
}
|
||||
}
|
||||
|
||||
&--success {
|
||||
background-color: var(--callout-success-background);
|
||||
color: var(--callout-success-description);
|
||||
.announcement-banner__action,
|
||||
.announcement-banner__dismiss {
|
||||
background: var(--callout-success-border);
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__message {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: var(--line-height-normal);
|
||||
|
||||
strong {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
}
|
||||
|
||||
&__action {
|
||||
height: 24px;
|
||||
font-size: var(--label-small-500-font-size);
|
||||
color: currentColor;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
&__dismiss {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
color: currentColor;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import {
|
||||
AnnouncementBanner,
|
||||
AnnouncementBannerProps,
|
||||
PersistedAnnouncementBanner,
|
||||
} from './index';
|
||||
|
||||
const STORAGE_KEY = 'test-banner-dismissed';
|
||||
|
||||
function renderBanner(props: Partial<AnnouncementBannerProps> = {}): void {
|
||||
render(<AnnouncementBanner message="Test message" {...props} />);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
});
|
||||
|
||||
describe('AnnouncementBanner', () => {
|
||||
it('renders message and default warning variant', () => {
|
||||
renderBanner({ message: <strong>Heads up</strong> });
|
||||
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toHaveClass('announcement-banner--warning');
|
||||
expect(alert).toHaveTextContent('Heads up');
|
||||
});
|
||||
|
||||
it.each(['warning', 'info', 'success', 'error'] as const)(
|
||||
'renders %s variant correctly',
|
||||
(type) => {
|
||||
renderBanner({ type, message: 'Test message' });
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toHaveClass(`announcement-banner--${type}`);
|
||||
},
|
||||
);
|
||||
|
||||
it('calls action onClick when action button is clicked', async () => {
|
||||
const onClick = jest.fn() as jest.MockedFunction<() => void>;
|
||||
renderBanner({ action: { label: 'Go to Settings', onClick } });
|
||||
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
await user.click(screen.getByRole('button', { name: /go to settings/i }));
|
||||
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('hides dismiss button when onClose is not provided and hides icon when icon is null', () => {
|
||||
renderBanner({ onClose: undefined, icon: null });
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /dismiss/i }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('alert')?.querySelector('.announcement-banner__icon'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PersistedAnnouncementBanner', () => {
|
||||
it('dismisses on click, calls onDismiss, and persists to localStorage', async () => {
|
||||
const onDismiss = jest.fn() as jest.MockedFunction<() => void>;
|
||||
render(
|
||||
<PersistedAnnouncementBanner
|
||||
message="Test message"
|
||||
storageKey={STORAGE_KEY}
|
||||
onDismiss={onDismiss}
|
||||
/>,
|
||||
);
|
||||
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
await user.click(screen.getByRole('button', { name: /dismiss/i }));
|
||||
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
expect(onDismiss).toHaveBeenCalledTimes(1);
|
||||
expect(localStorage.getItem(STORAGE_KEY)).toBe('true');
|
||||
});
|
||||
|
||||
it('does not render when storageKey is already set in localStorage', () => {
|
||||
localStorage.setItem(STORAGE_KEY, 'true');
|
||||
render(
|
||||
<PersistedAnnouncementBanner
|
||||
message="Test message"
|
||||
storageKey={STORAGE_KEY}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import {
|
||||
CircleAlert,
|
||||
CircleCheckBig,
|
||||
Info,
|
||||
TriangleAlert,
|
||||
X,
|
||||
} from '@signozhq/icons';
|
||||
import cx from 'classnames';
|
||||
|
||||
import './AnnouncementBanner.styles.scss';
|
||||
|
||||
export type AnnouncementBannerType = 'warning' | 'info' | 'error' | 'success';
|
||||
|
||||
export interface AnnouncementBannerAction {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export interface AnnouncementBannerProps {
|
||||
message: ReactNode;
|
||||
type?: AnnouncementBannerType;
|
||||
icon?: ReactNode | null;
|
||||
action?: AnnouncementBannerAction;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_ICONS: Record<AnnouncementBannerType, ReactNode> = {
|
||||
warning: <TriangleAlert size={14} />,
|
||||
info: <Info size={14} />,
|
||||
error: <CircleAlert size={14} />,
|
||||
success: <CircleCheckBig size={14} />,
|
||||
};
|
||||
|
||||
export default function AnnouncementBanner({
|
||||
message,
|
||||
type = 'warning',
|
||||
icon,
|
||||
action,
|
||||
onClose,
|
||||
className,
|
||||
}: AnnouncementBannerProps): JSX.Element {
|
||||
const resolvedIcon = icon === null ? null : icon ?? DEFAULT_ICONS[type];
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className={cx(
|
||||
'announcement-banner',
|
||||
`announcement-banner--${type}`,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="announcement-banner__body">
|
||||
{resolvedIcon && (
|
||||
<span className="announcement-banner__icon">{resolvedIcon}</span>
|
||||
)}
|
||||
<span className="announcement-banner__message">{message}</span>
|
||||
{action && (
|
||||
<Button
|
||||
type="button"
|
||||
className="announcement-banner__action"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{onClose && (
|
||||
<Button
|
||||
type="button"
|
||||
aria-label="Dismiss"
|
||||
className="announcement-banner__dismiss"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import AnnouncementBanner, {
|
||||
AnnouncementBannerProps,
|
||||
} from './AnnouncementBanner';
|
||||
|
||||
interface PersistedAnnouncementBannerProps extends AnnouncementBannerProps {
|
||||
storageKey: string;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
function isDismissed(storageKey: string): boolean {
|
||||
return localStorage.getItem(storageKey) === 'true';
|
||||
}
|
||||
|
||||
export default function PersistedAnnouncementBanner({
|
||||
storageKey,
|
||||
onDismiss,
|
||||
...props
|
||||
}: PersistedAnnouncementBannerProps): JSX.Element | null {
|
||||
const [visible, setVisible] = useState(() => !isDismissed(storageKey));
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClose = (): void => {
|
||||
localStorage.setItem(storageKey, 'true');
|
||||
setVisible(false);
|
||||
onDismiss?.();
|
||||
};
|
||||
|
||||
return <AnnouncementBanner {...props} onClose={handleClose} />;
|
||||
}
|
||||
12
frontend/src/components/AnnouncementBanner/index.ts
Normal file
12
frontend/src/components/AnnouncementBanner/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import AnnouncementBanner from './AnnouncementBanner';
|
||||
import PersistedAnnouncementBanner from './PersistedAnnouncementBanner';
|
||||
|
||||
export type {
|
||||
AnnouncementBannerAction,
|
||||
AnnouncementBannerProps,
|
||||
AnnouncementBannerType,
|
||||
} from './AnnouncementBanner';
|
||||
|
||||
export { AnnouncementBanner, PersistedAnnouncementBanner };
|
||||
|
||||
export default AnnouncementBanner;
|
||||
@@ -1,52 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { refreshIntervalOptions } from 'container/TopNav/AutoRefreshV2/constants';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useGlobalTimeStore } from 'store/globalTime/globalTimeStore';
|
||||
import { createCustomTimeRange } from 'store/globalTime/utils';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
/**
|
||||
* Adapter component that syncs Redux global time state to Zustand store.
|
||||
* This component should be rendered once at the app level.
|
||||
*
|
||||
* It reads from the Redux globalTime reducer and updates the Zustand store
|
||||
* to provide a migration path from Redux to Zustand.
|
||||
*/
|
||||
export function GlobalTimeStoreAdapter(): null {
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const setSelectedTime = useGlobalTimeStore((s) => s.setSelectedTime);
|
||||
|
||||
useEffect(() => {
|
||||
// Convert the selectedTime to the new format
|
||||
// If it's 'custom', store the min/max times in the custom format
|
||||
const selectedTime =
|
||||
globalTime.selectedTime === 'custom'
|
||||
? createCustomTimeRange(globalTime.minTime, globalTime.maxTime)
|
||||
: (globalTime.selectedTime as Time);
|
||||
|
||||
// Find refresh interval from Redux state
|
||||
const refreshOption = refreshIntervalOptions.find(
|
||||
(option) => option.key === globalTime.selectedAutoRefreshInterval,
|
||||
);
|
||||
|
||||
const refreshInterval =
|
||||
!globalTime.isAutoRefreshDisabled && refreshOption ? refreshOption.value : 0;
|
||||
|
||||
setSelectedTime(selectedTime, refreshInterval);
|
||||
}, [
|
||||
globalTime.selectedTime,
|
||||
globalTime.isAutoRefreshDisabled,
|
||||
globalTime.selectedAutoRefreshInterval,
|
||||
globalTime.minTime,
|
||||
globalTime.maxTime,
|
||||
setSelectedTime,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { act, render, renderHook } from '@testing-library/react';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import configureStore, { MockStoreEnhanced } from 'redux-mock-store';
|
||||
import { useGlobalTimeStore } from 'store/globalTime/globalTimeStore';
|
||||
import { createCustomTimeRange } from 'store/globalTime/utils';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { GlobalTimeStoreAdapter } from '../GlobalTimeStoreAdapter';
|
||||
|
||||
const mockStore = configureStore<Partial<AppState>>([]);
|
||||
|
||||
const randomTime = 1700000000000000000;
|
||||
|
||||
describe('GlobalTimeStoreAdapter', () => {
|
||||
let store: MockStoreEnhanced<Partial<AppState>>;
|
||||
|
||||
const createGlobalTimeState = (
|
||||
overrides: Partial<GlobalReducer> = {},
|
||||
): GlobalReducer => ({
|
||||
minTime: randomTime,
|
||||
maxTime: randomTime,
|
||||
loading: false,
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: true,
|
||||
selectedAutoRefreshInterval: 'off',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset Zustand store before each test
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
act(() => {
|
||||
result.current.setSelectedTime(DEFAULT_TIME_RANGE, 0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render null because it just an adapter', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState(),
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('should sync relative time from Redux to Zustand store', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: true,
|
||||
selectedAutoRefreshInterval: 'off',
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.selectedTime).toBe('15m');
|
||||
expect(result.current.refreshInterval).toBe(0);
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should sync custom time from Redux to Zustand store', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: 'custom',
|
||||
minTime: randomTime,
|
||||
maxTime: randomTime,
|
||||
isAutoRefreshDisabled: true,
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.selectedTime).toBe(
|
||||
createCustomTimeRange(randomTime, randomTime),
|
||||
);
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should sync refresh interval when auto refresh is enabled', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: false,
|
||||
selectedAutoRefreshInterval: '5s',
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.selectedTime).toBe('15m');
|
||||
expect(result.current.refreshInterval).toBe(5000); // 5s = 5000ms
|
||||
expect(result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should set refreshInterval to 0 when auto refresh is disabled', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: true,
|
||||
selectedAutoRefreshInterval: '5s', // Even with interval set, should be 0 when disabled
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.refreshInterval).toBe(0);
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should update Zustand store when Redux state changes', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: true,
|
||||
}),
|
||||
});
|
||||
|
||||
const { rerender } = render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
// Verify initial state
|
||||
let zustandState = renderHook(() => useGlobalTimeStore());
|
||||
expect(zustandState.result.current.selectedTime).toBe('15m');
|
||||
|
||||
// Update Redux store
|
||||
const newStore = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '1h',
|
||||
isAutoRefreshDisabled: false,
|
||||
selectedAutoRefreshInterval: '30s',
|
||||
}),
|
||||
});
|
||||
|
||||
rerender(
|
||||
<Provider store={newStore}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
// Verify updated state
|
||||
zustandState = renderHook(() => useGlobalTimeStore());
|
||||
expect(zustandState.result.current.selectedTime).toBe('1h');
|
||||
expect(zustandState.result.current.refreshInterval).toBe(30000); // 30s = 30000ms
|
||||
expect(zustandState.result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle various refresh interval options', () => {
|
||||
const testCases = [
|
||||
{ key: '5s', expectedValue: 5000 },
|
||||
{ key: '10s', expectedValue: 10000 },
|
||||
{ key: '30s', expectedValue: 30000 },
|
||||
{ key: '1m', expectedValue: 60000 },
|
||||
{ key: '5m', expectedValue: 300000 },
|
||||
];
|
||||
|
||||
testCases.forEach(({ key, expectedValue }) => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: false,
|
||||
selectedAutoRefreshInterval: key,
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.refreshInterval).toBe(expectedValue);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle unknown refresh interval by setting 0', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: false,
|
||||
selectedAutoRefreshInterval: 'unknown-interval',
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.refreshInterval).toBe(0);
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -165,17 +165,7 @@ function KeysTab({
|
||||
return (
|
||||
<div className="keys-tab__empty">
|
||||
<KeyRound size={24} className="keys-tab__empty-icon" />
|
||||
<p className="keys-tab__empty-text">
|
||||
No keys. Start by creating one.{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/manage/administrator-guide/iam/service-accounts/#step-3-generate-an-api-key"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="keys-tab__learn-more"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
<p className="keys-tab__empty-text">No keys. Start by creating one.</p>
|
||||
<Button
|
||||
type="button"
|
||||
className="keys-tab__learn-more"
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
export const REACT_QUERY_KEY = {
|
||||
/**
|
||||
* For any query that should support AutoRefresh and min/max time is from DateTimeSelectionV2
|
||||
* You can prefix the query with this KEY, it will allow the queries to be automatically refreshed
|
||||
* when the user clicks in the refresh button, or alert the user when the data is being refreshed.
|
||||
*/
|
||||
AUTO_REFRESH_QUERY: 'AUTO_REFRESH_QUERY',
|
||||
|
||||
GET_PUBLIC_DASHBOARD: 'GET_PUBLIC_DASHBOARD',
|
||||
GET_PUBLIC_DASHBOARD_META: 'GET_PUBLIC_DASHBOARD_META',
|
||||
GET_PUBLIC_DASHBOARD_WIDGET_DATA: 'GET_PUBLIC_DASHBOARD_WIDGET_DATA',
|
||||
|
||||
@@ -248,35 +248,5 @@ export function createShortcutActions(deps: ActionDeps): CmdAction[] {
|
||||
roles: ['ADMIN', 'EDITOR'],
|
||||
perform: (): void => navigate(ROUTES.BILLING),
|
||||
},
|
||||
{
|
||||
id: 'my-settings-service-accounts',
|
||||
name: 'Go to Service Accounts',
|
||||
shortcut: [GlobalShortcutsName.NavigateToSettingsServiceAccounts],
|
||||
keywords: 'settings service accounts',
|
||||
section: 'Settings',
|
||||
icon: <Settings size={14} />,
|
||||
roles: ['ADMIN'],
|
||||
perform: (): void => navigate(ROUTES.SERVICE_ACCOUNTS_SETTINGS),
|
||||
},
|
||||
{
|
||||
id: 'my-settings-roles',
|
||||
name: 'Go to Roles',
|
||||
shortcut: [GlobalShortcutsName.NavigateToSettingsRoles],
|
||||
keywords: 'settings roles',
|
||||
section: 'Settings',
|
||||
icon: <Settings size={14} />,
|
||||
roles: ['ADMIN'],
|
||||
perform: (): void => navigate(ROUTES.ROLES_SETTINGS),
|
||||
},
|
||||
{
|
||||
id: 'my-settings-members',
|
||||
name: 'Go to Members',
|
||||
shortcut: [GlobalShortcutsName.NavigateToSettingsMembers],
|
||||
keywords: 'settings members',
|
||||
section: 'Settings',
|
||||
icon: <Settings size={14} />,
|
||||
roles: ['ADMIN'],
|
||||
perform: (): void => navigate(ROUTES.MEMBERS_SETTINGS),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -27,9 +27,6 @@ export const GlobalShortcuts = {
|
||||
NavigateToSettingsIngestion: 'shift+g+i',
|
||||
NavigateToSettingsBilling: 'shift+g+b',
|
||||
NavigateToSettingsNotificationChannels: 'shift+g+n',
|
||||
NavigateToSettingsServiceAccounts: 'shift+g+k',
|
||||
NavigateToSettingsRoles: 'shift+g+r',
|
||||
NavigateToSettingsMembers: 'shift+g+m',
|
||||
};
|
||||
|
||||
export const GlobalShortcutsName = {
|
||||
@@ -50,9 +47,6 @@ export const GlobalShortcutsName = {
|
||||
NavigateToSettingsIngestion: 'shift+g+i',
|
||||
NavigateToSettingsBilling: 'shift+g+b',
|
||||
NavigateToSettingsNotificationChannels: 'shift+g+n',
|
||||
NavigateToSettingsServiceAccounts: 'shift+g+k',
|
||||
NavigateToSettingsRoles: 'shift+g+r',
|
||||
NavigateToSettingsMembers: 'shift+g+m',
|
||||
NavigateToLogs: 'shift+l',
|
||||
NavigateToLogsPipelines: 'shift+l+p',
|
||||
NavigateToLogsViews: 'shift+l+v',
|
||||
@@ -80,7 +74,4 @@ export const GlobalShortcutsDescription = {
|
||||
'Navigate to Notification Channels Settings',
|
||||
NavigateToLogsPipelines: 'Navigate to Logs Pipelines',
|
||||
NavigateToLogsViews: 'Navigate to Logs Views',
|
||||
NavigateToSettingsServiceAccounts: 'Navigate to Service Accounts Settings',
|
||||
NavigateToSettingsRoles: 'Navigate to Roles Settings',
|
||||
NavigateToSettingsMembers: 'Navigate to Members Settings',
|
||||
};
|
||||
|
||||
@@ -3,12 +3,12 @@ import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Compass, Dot, House, Plus, Wrench } from '@signozhq/icons';
|
||||
import { PersistedAnnouncementBanner } from '@signozhq/ui';
|
||||
import { Button, Popover } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useGetMetricsOnboardingStatus } from 'api/generated/services/metrics';
|
||||
import listUserPreferences from 'api/v1/user/preferences/list';
|
||||
import updateUserPreferenceAPI from 'api/v1/user/preferences/name/update';
|
||||
import { PersistedAnnouncementBanner } from 'components/AnnouncementBanner';
|
||||
import Header from 'components/Header/Header';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
@@ -265,19 +265,20 @@ export default function Home(): JSX.Element {
|
||||
return (
|
||||
<div className="home-container">
|
||||
<PersistedAnnouncementBanner
|
||||
type="info"
|
||||
type="warning"
|
||||
storageKey={LOCALSTORAGE.DISMISSED_API_KEYS_DEPRECATION_BANNER}
|
||||
message={
|
||||
<>
|
||||
<strong>API Keys</strong> have been deprecated and replaced by{' '}
|
||||
<strong>Service Accounts</strong>. Please migrate to Service Accounts for
|
||||
programmatic API access.
|
||||
</>
|
||||
}
|
||||
action={{
|
||||
label: 'Go to Service Accounts',
|
||||
onClick: (): void => history.push(ROUTES.SERVICE_ACCOUNTS_SETTINGS),
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<strong>API keys</strong> have been deprecated in favour of{' '}
|
||||
<strong>Service accounts</strong>. The existing API Keys have been migrated
|
||||
to service accounts.
|
||||
</>
|
||||
</PersistedAnnouncementBanner>
|
||||
/>
|
||||
|
||||
<div className="sticky-header">
|
||||
<Header
|
||||
|
||||
@@ -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',
|
||||
@@ -0,0 +1,3 @@
|
||||
import JobDetails from './JobDetails';
|
||||
|
||||
export default JobDetails;
|
||||
@@ -0,0 +1,62 @@
|
||||
.infra-monitoring-container {
|
||||
.jobs-list-table {
|
||||
.expanded-table-container {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.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(.job-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-jobs-list-table {
|
||||
.ant-table-cell {
|
||||
min-width: 200px !important;
|
||||
max-width: 200px !important;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
}
|
||||
@@ -1,116 +1,683 @@
|
||||
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 { K8sJobsListPayload } from 'api/infraMonitoring/getK8sJobsList';
|
||||
import classNames from 'classnames';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useGetK8sJobsList } from 'hooks/infraMonitoring/useGetK8sJobsList';
|
||||
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 { getK8sJobsList, K8sJobsData } from './api';
|
||||
import {
|
||||
getJobMetricsQueryPayload,
|
||||
jobWidgetInfo,
|
||||
k8sJobDetailsMetadataConfig,
|
||||
k8sJobGetEntityName,
|
||||
k8sJobGetSelectedItemFilters,
|
||||
k8sJobInitialEventsFilter,
|
||||
k8sJobInitialFilters,
|
||||
k8sJobInitialLogTracesFilter,
|
||||
} from './constants';
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
K8sCategory,
|
||||
} from '../constants';
|
||||
import {
|
||||
k8sJobsColumns,
|
||||
k8sJobsColumnsConfig,
|
||||
k8sJobsRenderRowData,
|
||||
} from './table';
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringJobUID,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
import K8sHeader from '../K8sHeader';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
import { usePageSize } from '../utils';
|
||||
import JobDetails from './JobDetails';
|
||||
import {
|
||||
defaultAddedColumns,
|
||||
formatDataForTable,
|
||||
getK8sJobsListColumns,
|
||||
getK8sJobsListQuery,
|
||||
K8sJobsRowData,
|
||||
} from './utils';
|
||||
|
||||
import '../InfraMonitoringK8s.styles.scss';
|
||||
import './K8sJobsList.styles.scss';
|
||||
|
||||
function K8sJobsList({
|
||||
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 [selectedJobUID, setselectedJobUID] = useInfraMonitoringJobUID();
|
||||
|
||||
const { pageSize, setPageSize } = usePageSize(K8sCategory.JOBS);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
|
||||
const [, setView] = useInfraMonitoringView();
|
||||
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
|
||||
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
|
||||
const [, setLogFilters] = useInfraMonitoringLogFilters();
|
||||
|
||||
const [selectedRowData, setSelectedRowData] = useState<K8sJobsRowData | 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: K8sJobsRowData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): IBuilderQuery['filters'] => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...queryFilters.items],
|
||||
op: 'and',
|
||||
};
|
||||
|
||||
const response = await getK8sJobsList(
|
||||
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: K8sJobsData | null; error?: string | null }> => {
|
||||
const response = await getK8sJobsList(
|
||||
{
|
||||
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 = getK8sJobsListQuery();
|
||||
|
||||
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 (selectedJobUID) {
|
||||
return [
|
||||
'jobList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'jobList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [queryFilters, orderBy, selectedJobUID, minTime, maxTime, selectedRowData]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
isLoading: isLoadingGroupedByRowData,
|
||||
isError: isErrorGroupedByRowData,
|
||||
refetch: fetchGroupedByRowData,
|
||||
} = useGetK8sJobsList(
|
||||
fetchGroupedByRowDataQuery as K8sJobsListPayload,
|
||||
{
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: GetK8sEntityToAggregateAttribute(
|
||||
K8sCategory.JOBS,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true,
|
||||
K8sCategory.JOBS,
|
||||
);
|
||||
|
||||
const query = useMemo(() => {
|
||||
const baseQuery = getK8sJobsListQuery();
|
||||
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 formattedGroupedByJobsData = useMemo(
|
||||
() =>
|
||||
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const nestedJobsData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) {
|
||||
return [];
|
||||
}
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedJobUID) {
|
||||
return [
|
||||
'jobList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'jobList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedJobUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sJobsList(
|
||||
query as K8sJobsListPayload,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const jobsData = useMemo(() => data?.payload?.data?.records || [], [data]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const formattedJobsData = useMemo(
|
||||
() => formatDataForTable(jobsData, groupBy),
|
||||
[jobsData, groupBy],
|
||||
);
|
||||
|
||||
const columns = useMemo(() => getK8sJobsListColumns(groupBy), [groupBy]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sJobsRowData): 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<K8sJobsRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter: SorterResult<K8sJobsRowData> | SorterResult<K8sJobsRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
});
|
||||
}
|
||||
|
||||
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.Job,
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
total: data?.payload?.data?.total,
|
||||
});
|
||||
}, [data?.payload?.data?.total]);
|
||||
|
||||
const selectedJobData = useMemo(() => {
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the job from the formatted grouped by data
|
||||
return nestedJobsData.find((job) => job.jobName === selectedJobUID) || null;
|
||||
}
|
||||
// If not grouped by, return the job from the jobs data
|
||||
return jobsData.find((job) => job.jobName === selectedJobUID) || null;
|
||||
}, [selectedJobUID, groupBy.length, jobsData, nestedJobsData]);
|
||||
|
||||
const openJobInNewTab = (record: K8sJobsRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID, record.jobUID);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sJobsRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openJobInNewTab(record);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedJobUID(record.jobUID);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
});
|
||||
};
|
||||
|
||||
const nestedColumns = useMemo(() => getK8sJobsListColumns([]), []);
|
||||
|
||||
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<K8sJobsRowData>[]}
|
||||
dataSource={formattedGroupedByJobsData}
|
||||
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)) {
|
||||
openJobInNewTab(record);
|
||||
return;
|
||||
}
|
||||
setselectedJobUID(record.jobUID);
|
||||
},
|
||||
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: K8sJobsRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sJobsRowData;
|
||||
}): 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 handleCloseJobDetail = (): void => {
|
||||
setselectedJobUID(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.Job,
|
||||
});
|
||||
},
|
||||
[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.Job,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<K8sJobsData>
|
||||
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.JOBS}
|
||||
tableColumnsDefinitions={k8sJobsColumns}
|
||||
tableColumns={k8sJobsColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sJobsRenderRowData}
|
||||
eventCategory={InfraMonitoringEvents.Job}
|
||||
showAutoRefresh={!selectedJobData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
<Table
|
||||
className={classNames('k8s-list-table', 'jobs-list-table', {
|
||||
'expanded-jobs-list-table': isGroupedByAttribute,
|
||||
})}
|
||||
dataSource={isFetching || isLoading ? [] : formattedJobsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: onPaginationChange,
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
locale={{
|
||||
emptyText:
|
||||
isFetching || isLoading ? 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<K8sJobsData>
|
||||
category={K8sCategory.JOBS}
|
||||
eventCategory={InfraMonitoringEvents.Job}
|
||||
getSelectedItemFilters={k8sJobGetSelectedItemFilters}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sJobGetEntityName}
|
||||
getInitialLogTracesFilters={k8sJobInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sJobInitialEventsFilter}
|
||||
primaryFilterKeys={k8sJobInitialFilters}
|
||||
metadataConfig={k8sJobDetailsMetadataConfig}
|
||||
entityWidgetInfo={jobWidgetInfo}
|
||||
getEntityQueryPayload={getJobMetricsQueryPayload}
|
||||
queryKeyPrefix="job"
|
||||
<JobDetails
|
||||
job={selectedJobData}
|
||||
isModalTimeSelection
|
||||
onClose={handleCloseJobDetail}
|
||||
/>
|
||||
</>
|
||||
</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,330 +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 { K8sJobsData } from './api';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export const k8sJobsColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Job Group',
|
||||
value: 'jobGroup',
|
||||
id: 'jobGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'Job Name',
|
||||
value: 'jobName',
|
||||
id: 'jobName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Successful',
|
||||
value: 'successful_pods',
|
||||
id: 'successful_pods',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Failed',
|
||||
value: 'failed_pods',
|
||||
id: 'failed_pods',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Desired Successful',
|
||||
value: 'desired_successful_pods',
|
||||
id: 'desired_successful_pods',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Active',
|
||||
value: 'active_pods',
|
||||
id: 'active_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 k8sJobsColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> JOB GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'jobGroup',
|
||||
key: 'jobGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
},
|
||||
{
|
||||
title: <div>Job Name</div>,
|
||||
dataIndex: 'jobName',
|
||||
key: 'jobName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Namespace Name</div>,
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Successful</div>,
|
||||
dataIndex: 'successful_pods',
|
||||
key: 'successful_pods',
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Failed</div>,
|
||||
dataIndex: 'failed_pods',
|
||||
key: 'failed_pods',
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Desired Successful</div>,
|
||||
dataIndex: 'desired_successful_pods',
|
||||
key: 'desired_successful_pods',
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Active</div>,
|
||||
dataIndex: 'active_pods',
|
||||
key: 'active_pods',
|
||||
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: 120,
|
||||
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 k8sJobsRenderRowData = (
|
||||
job: K8sJobsData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(job, () => job.jobName || job.meta.k8s_job_name || '', groupBy),
|
||||
itemKey: job.meta.k8s_job_name,
|
||||
jobName: (
|
||||
<Tooltip title={job.meta.k8s_job_name}>{job.meta.k8s_job_name || ''}</Tooltip>
|
||||
),
|
||||
namespaceName: (
|
||||
<Tooltip title={job.meta.k8s_namespace_name}>
|
||||
{job.meta.k8s_namespace_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={job.cpuRequest}
|
||||
entity={K8sCategory.JOBS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={job.cpuRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={job.cpuLimit}
|
||||
entity={K8sCategory.JOBS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={job.cpuLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={job.cpuUsage}>
|
||||
{job.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={job.memoryRequest}
|
||||
entity={K8sCategory.JOBS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={job.memoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={job.memoryLimit}
|
||||
entity={K8sCategory.JOBS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={job.memoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={job.memoryUsage}>
|
||||
{formatBytes(job.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
successful_pods: (
|
||||
<ValidateColumnValueWrapper value={job.successfulPods}>
|
||||
{job.successfulPods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
desired_successful_pods: (
|
||||
<ValidateColumnValueWrapper value={job.desiredSuccessfulPods}>
|
||||
{job.desiredSuccessfulPods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
failed_pods: (
|
||||
<ValidateColumnValueWrapper value={job.failedPods}>
|
||||
{job.failedPods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
active_pods: (
|
||||
<ValidateColumnValueWrapper value={job.activePods}>
|
||||
{job.activePods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
jobGroup: getGroupByEl(job, groupBy),
|
||||
...job.meta,
|
||||
groupedByMeta: getGroupedByMeta(job, groupBy),
|
||||
});
|
||||
393
frontend/src/container/InfraMonitoringK8s/Jobs/utils.tsx
Normal file
393
frontend/src/container/InfraMonitoringK8s/Jobs/utils.tsx
Normal file
@@ -0,0 +1,393 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
|
||||
import {
|
||||
K8sJobsData,
|
||||
K8sJobsListPayload,
|
||||
} from 'api/infraMonitoring/getK8sJobsList';
|
||||
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: 'Job Name',
|
||||
value: 'jobName',
|
||||
id: 'jobName',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Successful',
|
||||
value: 'successful_pods',
|
||||
id: 'successful_pods',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Failed',
|
||||
value: 'failed_pods',
|
||||
id: 'failed_pods',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Desired Successful',
|
||||
value: 'desired_successful_pods',
|
||||
id: 'desired_successful_pods',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Active',
|
||||
value: 'active_pods',
|
||||
id: 'active_pods',
|
||||
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 K8sJobsRowData {
|
||||
key: string;
|
||||
jobUID: string;
|
||||
jobName: React.ReactNode;
|
||||
namespaceName: React.ReactNode;
|
||||
successful_pods: React.ReactNode;
|
||||
failed_pods: React.ReactNode;
|
||||
active_pods: React.ReactNode;
|
||||
desired_successful_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;
|
||||
groupedByMeta?: any;
|
||||
}
|
||||
|
||||
const jobGroupColumnConfig = {
|
||||
title: (
|
||||
<div className="column-header entity-group-header">
|
||||
<Group size={14} /> JOB GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'jobGroup',
|
||||
key: 'jobGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
className: 'column entity-group-header',
|
||||
};
|
||||
|
||||
export const getK8sJobsListQuery = (): K8sJobsListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
const columnProgressBarClassName = 'column-progress-bar';
|
||||
|
||||
const columnsConfig = [
|
||||
{
|
||||
title: <div className="column-header-left job-name-header">Job Name</div>,
|
||||
dataIndex: 'jobName',
|
||||
key: 'jobName',
|
||||
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">Successful</div>,
|
||||
dataIndex: 'successful_pods',
|
||||
key: 'successful_pods',
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header small-col">Failed</div>,
|
||||
dataIndex: 'failed_pods',
|
||||
key: 'failed_pods',
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header small-col">Desired Successful</div>,
|
||||
dataIndex: 'desired_successful_pods',
|
||||
key: 'desired_successful_pods',
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header small-col">Active</div>,
|
||||
dataIndex: 'active_pods',
|
||||
key: 'active_pods',
|
||||
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 small-col">Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
];
|
||||
|
||||
export const getK8sJobsListColumns = (
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): ColumnType<K8sJobsRowData>[] => {
|
||||
if (groupBy.length > 0) {
|
||||
const filteredColumns = [...columnsConfig].filter(
|
||||
(column) => column.key !== 'jobName',
|
||||
);
|
||||
filteredColumns.unshift(jobGroupColumnConfig);
|
||||
return filteredColumns as ColumnType<K8sJobsRowData>[];
|
||||
}
|
||||
|
||||
return columnsConfig as ColumnType<K8sJobsRowData>[];
|
||||
};
|
||||
|
||||
const dotToUnder: Record<string, keyof K8sJobsData['meta']> = {
|
||||
'k8s.job.name': 'k8s_job_name',
|
||||
'k8s.namespace.name': 'k8s_namespace_name',
|
||||
'k8s.cluster.name': 'k8s_cluster_name',
|
||||
};
|
||||
|
||||
const getGroupByEle = (
|
||||
job: K8sJobsData,
|
||||
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 job.meta;
|
||||
const value = job.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: K8sJobsData[],
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): K8sJobsRowData[] =>
|
||||
data.map((job, index) => ({
|
||||
key: index.toString(),
|
||||
jobUID: job.jobName,
|
||||
jobName: (
|
||||
<Tooltip title={job.meta.k8s_job_name}>
|
||||
{job.meta.k8s_job_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
namespaceName: (
|
||||
<Tooltip title={job.meta.k8s_namespace_name}>
|
||||
{job.meta.k8s_namespace_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={job.cpuRequest}
|
||||
entity={K8sCategory.JOBS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={job.cpuRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={job.cpuLimit}
|
||||
entity={K8sCategory.JOBS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={job.cpuLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={job.cpuUsage}>
|
||||
{job.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={job.memoryRequest}
|
||||
entity={K8sCategory.JOBS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={job.memoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={job.memoryLimit}
|
||||
entity={K8sCategory.JOBS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={job.memoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={job.memoryUsage}>
|
||||
{formatBytes(job.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
successful_pods: (
|
||||
<ValidateColumnValueWrapper value={job.successfulPods}>
|
||||
{job.successfulPods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
desired_successful_pods: (
|
||||
<ValidateColumnValueWrapper value={job.desiredSuccessfulPods}>
|
||||
{job.desiredSuccessfulPods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
failed_pods: (
|
||||
<ValidateColumnValueWrapper value={job.failedPods}>
|
||||
{job.failedPods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
active_pods: (
|
||||
<ValidateColumnValueWrapper value={job.activePods}>
|
||||
{job.activePods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
jobGroup: getGroupByEle(job, groupBy),
|
||||
meta: job.meta,
|
||||
...job.meta,
|
||||
groupedByMeta: job.meta,
|
||||
}));
|
||||
175
frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx
Normal file
175
frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Button, Select } from 'antd';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { Filter, SlidersHorizontal } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { K8sCategory } from './constants';
|
||||
import { useInfraMonitoringFiltersK8s } from './hooks';
|
||||
import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel';
|
||||
import { IEntityColumn } from './utils';
|
||||
|
||||
import './InfraMonitoringK8s.styles.scss';
|
||||
|
||||
interface K8sHeaderProps {
|
||||
selectedGroupBy: BaseAutocompleteData[];
|
||||
groupByOptions: { value: string; label: string }[];
|
||||
isLoadingGroupByFilters: boolean;
|
||||
handleFiltersChange: (value: IBuilderQuery['filters']) => void;
|
||||
handleGroupByChange: (value: IBuilderQuery['groupBy']) => void;
|
||||
defaultAddedColumns: IEntityColumn[];
|
||||
addedColumns?: IEntityColumn[];
|
||||
availableColumns?: IEntityColumn[];
|
||||
onAddColumn?: (column: IEntityColumn) => void;
|
||||
onRemoveColumn?: (column: IEntityColumn) => void;
|
||||
handleFilterVisibilityChange: () => void;
|
||||
isFiltersVisible: boolean;
|
||||
entity: K8sCategory;
|
||||
showAutoRefresh: boolean;
|
||||
}
|
||||
|
||||
function K8sHeader({
|
||||
selectedGroupBy,
|
||||
defaultAddedColumns,
|
||||
groupByOptions,
|
||||
isLoadingGroupByFilters,
|
||||
addedColumns,
|
||||
availableColumns,
|
||||
handleFiltersChange,
|
||||
handleGroupByChange,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
handleFilterVisibilityChange,
|
||||
isFiltersVisible,
|
||||
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 = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||
|
||||
const handleChangeTagFilters = useCallback(
|
||||
(value: IBuilderQuery['filters']) => {
|
||||
handleFiltersChange(value);
|
||||
setUrlFilters(value || null);
|
||||
},
|
||||
[handleFiltersChange, setUrlFilters],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="k8s-list-controls">
|
||||
<div className="k8s-list-controls-left">
|
||||
{!isFiltersVisible && (
|
||||
<div className="quick-filters-toggle-container">
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={handleFilterVisibilityChange}
|
||||
>
|
||||
<Filter size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="k8s-qb-search-container">
|
||||
<QueryBuilderSearch
|
||||
query={query as IBuilderQuery}
|
||||
onChange={handleChangeTagFilters}
|
||||
isInfraMonitoring
|
||||
disableNavigationShortcuts
|
||||
entity={entity}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="k8s-attribute-search-container">
|
||||
<div className="group-by-label"> Group by </div>
|
||||
<Select
|
||||
className="group-by-select"
|
||||
loading={isLoadingGroupByFilters}
|
||||
mode="multiple"
|
||||
value={selectedGroupBy}
|
||||
allowClear
|
||||
maxTagCount="responsive"
|
||||
placeholder="Search for attribute"
|
||||
style={{ width: '100%' }}
|
||||
options={groupByOptions}
|
||||
onChange={handleGroupByChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="k8s-list-controls-right">
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="text"
|
||||
className="periscope-btn ghost"
|
||||
disabled={selectedGroupBy?.length > 0}
|
||||
onClick={(): void => setIsFiltersSidePanelOpen(true)}
|
||||
>
|
||||
<SlidersHorizontal size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isFiltersSidePanelOpen && (
|
||||
<K8sFiltersSidePanel
|
||||
defaultAddedColumns={defaultAddedColumns}
|
||||
addedColumns={addedColumns}
|
||||
availableColumns={availableColumns}
|
||||
onClose={(): void => {
|
||||
if (isFiltersSidePanelOpen) {
|
||||
setIsFiltersSidePanelOpen(false);
|
||||
}
|
||||
}}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
K8sHeader.defaultProps = {
|
||||
addedColumns: [],
|
||||
availableColumns: [],
|
||||
onAddColumn: () => {},
|
||||
onRemoveColumn: () => {},
|
||||
};
|
||||
|
||||
export default K8sHeader;
|
||||
@@ -0,0 +1,17 @@
|
||||
.infra-monitoring-container {
|
||||
.namespaces-list-table {
|
||||
.expanded-table-container {
|
||||
padding-left: 80px;
|
||||
}
|
||||
|
||||
.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,716 @@
|
||||
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 { K8sNamespacesListPayload } from 'api/infraMonitoring/getK8sNamespacesList';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useGetK8sNamespacesList } from 'hooks/infraMonitoring/useGetK8sNamespacesList';
|
||||
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 { getK8sNamespacesList, K8sNamespacesData } from './api';
|
||||
import {
|
||||
getNamespaceMetricsQueryPayload,
|
||||
k8sNamespaceDetailsMetadataConfig,
|
||||
k8sNamespaceGetEntityName,
|
||||
k8sNamespaceGetSelectedItemFilters,
|
||||
k8sNamespaceInitialEventsFilter,
|
||||
k8sNamespaceInitialFilters,
|
||||
k8sNamespaceInitialLogTracesFilter,
|
||||
namespaceWidgetInfo,
|
||||
} from './constants';
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
K8sCategory,
|
||||
} from '../constants';
|
||||
import {
|
||||
k8sNamespacesColumns,
|
||||
k8sNamespacesColumnsConfig,
|
||||
k8sNamespacesRenderRowData,
|
||||
} from './table';
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringNamespaceUID,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
import K8sHeader from '../K8sHeader';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
import { usePageSize } from '../utils';
|
||||
import NamespaceDetails from './NamespaceDetails';
|
||||
import {
|
||||
defaultAddedColumns,
|
||||
formatDataForTable,
|
||||
getK8sNamespacesListColumns,
|
||||
getK8sNamespacesListQuery,
|
||||
K8sNamespacesRowData,
|
||||
} from './utils';
|
||||
|
||||
import '../InfraMonitoringK8s.styles.scss';
|
||||
import './K8sNamespacesList.styles.scss';
|
||||
|
||||
function K8sNamespacesList({
|
||||
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 [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [filtersInitialised, setFiltersInitialised] = useState(false);
|
||||
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
|
||||
const [
|
||||
selectedNamespaceUID,
|
||||
setselectedNamespaceUID,
|
||||
] = useInfraMonitoringNamespaceUID();
|
||||
|
||||
const { pageSize, setPageSize } = usePageSize(K8sCategory.NAMESPACES);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
|
||||
const [, setView] = useInfraMonitoringView();
|
||||
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
|
||||
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
|
||||
const [, setLogFilters] = useInfraMonitoringLogFilters();
|
||||
|
||||
const [
|
||||
selectedRowData,
|
||||
setSelectedRowData,
|
||||
] = useState<K8sNamespacesRowData | 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: K8sNamespacesRowData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): IBuilderQuery['filters'] => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...queryFilters.items],
|
||||
op: 'and',
|
||||
};
|
||||
|
||||
const response = await getK8sNamespacesList(
|
||||
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: K8sNamespacesData | null; error?: string | null }> => {
|
||||
const response = await getK8sNamespacesList(
|
||||
{
|
||||
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 = getK8sNamespacesListQuery();
|
||||
|
||||
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 (selectedNamespaceUID) {
|
||||
return [
|
||||
'namespaceList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'namespaceList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
queryFilters,
|
||||
orderBy,
|
||||
selectedNamespaceUID,
|
||||
minTime,
|
||||
maxTime,
|
||||
selectedRowData,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
isLoading: isLoadingGroupedByRowData,
|
||||
isError: isErrorGroupedByRowData,
|
||||
refetch: fetchGroupedByRowData,
|
||||
} = useGetK8sNamespacesList(
|
||||
fetchGroupedByRowDataQuery as K8sNamespacesListPayload,
|
||||
{
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: GetK8sEntityToAggregateAttribute(
|
||||
K8sCategory.NAMESPACES,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true,
|
||||
K8sCategory.NODES,
|
||||
);
|
||||
|
||||
const query = useMemo(() => {
|
||||
const baseQuery = getK8sNamespacesListQuery();
|
||||
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 formattedGroupedByNamespacesData = useMemo(
|
||||
() =>
|
||||
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedNamespaceUID) {
|
||||
return [
|
||||
'namespaceList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'namespaceList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedNamespaceUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sNamespacesList(
|
||||
query as K8sNamespacesListPayload,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const namespacesData = useMemo(() => data?.payload?.data?.records || [], [
|
||||
data,
|
||||
]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const formattedNamespacesData = useMemo(
|
||||
() => formatDataForTable(namespacesData, groupBy),
|
||||
[namespacesData, groupBy],
|
||||
);
|
||||
|
||||
const nestedNamespacesData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) {
|
||||
return [];
|
||||
}
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const columns = useMemo(() => getK8sNamespacesListColumns(groupBy), [groupBy]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sNamespacesRowData): 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<K8sNamespacesRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter:
|
||||
| SorterResult<K8sNamespacesRowData>
|
||||
| SorterResult<K8sNamespacesRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Namespace,
|
||||
});
|
||||
}
|
||||
|
||||
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.Namespace,
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Namespace,
|
||||
total: data?.payload?.data?.total,
|
||||
});
|
||||
}, [data?.payload?.data?.total]);
|
||||
|
||||
const selectedNamespaceData = useMemo(() => {
|
||||
if (!selectedNamespaceUID) {
|
||||
return null;
|
||||
}
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the namespace from the formatted grouped by namespaces data
|
||||
return (
|
||||
nestedNamespacesData.find(
|
||||
(namespace) => namespace.namespaceName === selectedNamespaceUID,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
// If not grouped by, return the node from the nodes data
|
||||
return (
|
||||
namespacesData.find(
|
||||
(namespace) => namespace.namespaceName === selectedNamespaceUID,
|
||||
) || null
|
||||
);
|
||||
}, [
|
||||
selectedNamespaceUID,
|
||||
groupBy.length,
|
||||
namespacesData,
|
||||
nestedNamespacesData,
|
||||
]);
|
||||
|
||||
const openNamespaceInNewTab = (record: K8sNamespacesRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
|
||||
record.namespaceUID,
|
||||
);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sNamespacesRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openNamespaceInNewTab(record);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedNamespaceUID(record.namespaceUID);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Namespace,
|
||||
});
|
||||
};
|
||||
|
||||
const nestedColumns = useMemo(() => getK8sNamespacesListColumns([]), []);
|
||||
|
||||
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<K8sNamespacesRowData>[]}
|
||||
dataSource={formattedGroupedByNamespacesData}
|
||||
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 (isModifierKeyPressed(event)) {
|
||||
openNamespaceInNewTab(record);
|
||||
return;
|
||||
}
|
||||
setselectedNamespaceUID(record.namespaceUID);
|
||||
},
|
||||
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: K8sNamespacesRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sNamespacesRowData;
|
||||
}): 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 handleCloseNamespaceDetail = (): void => {
|
||||
setselectedNamespaceUID(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.Namespace,
|
||||
});
|
||||
},
|
||||
[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.Namespace,
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedNamespacesData.length === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<K8sNamespacesData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={K8sCategory.NAMESPACES}
|
||||
tableColumnsDefinitions={k8sNamespacesColumns}
|
||||
tableColumns={k8sNamespacesColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sNamespacesRenderRowData}
|
||||
eventCategory={InfraMonitoringEvents.Namespace}
|
||||
<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={!selectedNamespaceData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
<K8sBaseDetails<K8sNamespacesData>
|
||||
category={K8sCategory.NAMESPACES}
|
||||
eventCategory={InfraMonitoringEvents.Namespace}
|
||||
getSelectedItemFilters={k8sNamespaceGetSelectedItemFilters}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sNamespaceGetEntityName}
|
||||
getInitialLogTracesFilters={k8sNamespaceInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sNamespaceInitialEventsFilter}
|
||||
primaryFilterKeys={k8sNamespaceInitialFilters}
|
||||
metadataConfig={k8sNamespaceDetailsMetadataConfig}
|
||||
entityWidgetInfo={namespaceWidgetInfo}
|
||||
getEntityQueryPayload={getNamespaceMetricsQueryPayload}
|
||||
queryKeyPrefix="namespace"
|
||||
<Table
|
||||
className="k8s-list-table namespaces-list-table"
|
||||
dataSource={showTableLoadingState ? [] : formattedNamespacesData}
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<NamespaceDetails
|
||||
namespace={selectedNamespaceData}
|
||||
isModalTimeSelection
|
||||
onClose={handleCloseNamespaceDetail}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { K8sNamespacesData } from 'api/infraMonitoring/getK8sNamespacesList';
|
||||
|
||||
export type NamespaceDetailsProps = {
|
||||
namespace: K8sNamespacesData | null;
|
||||
isModalTimeSelection: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -0,0 +1,249 @@
|
||||
.namespace-detail-drawer {
|
||||
border-left: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.ant-drawer-header {
|
||||
padding: 8px 16px;
|
||||
border-bottom: none;
|
||||
|
||||
align-items: stretch;
|
||||
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--text-vanilla-400);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: var(--padding-1);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.namespace-detail-drawer__namespace {
|
||||
.namespace-details-grid {
|
||||
.labels-row,
|
||||
.values-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.5fr 1.5fr 1.5fr;
|
||||
gap: 30px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.labels-row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.namespace-details-metadata-label {
|
||||
color: var(--text-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.namespace-details-metadata-value {
|
||||
color: var(--text-vanilla-400);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
margin: 0;
|
||||
|
||||
&.active {
|
||||
color: var(--success-500);
|
||||
background: var(--success-100);
|
||||
border-color: var(--success-500);
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
color: var(--error-500);
|
||||
background: var(--error-100);
|
||||
border-color: var(--error-500);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 158px;
|
||||
|
||||
.ant-progress {
|
||||
margin: 0;
|
||||
|
||||
.ant-progress-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
&.ant-card-bordered {
|
||||
border: 1px solid var(--bg-slate-500) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-and-search {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 16px 0;
|
||||
|
||||
.action-btn {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.views-tabs-container {
|
||||
margin-top: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.views-tabs {
|
||||
color: var(--text-vanilla-400);
|
||||
|
||||
.view-title {
|
||||
display: flex;
|
||||
gap: var(--margin-2);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-xs);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
.tab {
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
width: 114px;
|
||||
}
|
||||
|
||||
.tab::before {
|
||||
background: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.selected_view {
|
||||
background: var(--bg-slate-300);
|
||||
color: var(--text-vanilla-100);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.selected_view::before {
|
||||
background: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
.compass-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.ant-drawer-header {
|
||||
border-bottom: 1px solid var(--bg-vanilla-400);
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.namespace-detail-drawer {
|
||||
.title {
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.namespace-detail-drawer__namespace {
|
||||
.ant-typography {
|
||||
color: var(--text-ink-300);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
border: 1px solid var(--bg-vanilla-400);
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.views-tabs {
|
||||
.tab {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.selected_view {
|
||||
background: var(--bg-vanilla-300);
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
color: var(--text-ink-400);
|
||||
}
|
||||
|
||||
.selected_view::before {
|
||||
background: var(--bg-vanilla-300);
|
||||
border-left: 1px solid var(--bg-slate-300);
|
||||
}
|
||||
}
|
||||
|
||||
.compass-button {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tabs-and-search {
|
||||
.action-btn {
|
||||
border: 1px solid var(--bg-vanilla-400);
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,640 @@
|
||||
/* 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 { K8sNamespacesData } from 'api/infraMonitoring/getK8sNamespacesList';
|
||||
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 NamespaceEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
import NamespaceLogs from '../../EntityDetailsUtils/EntityLogs';
|
||||
import NamespaceMetrics from '../../EntityDetailsUtils/EntityMetrics';
|
||||
import NamespaceTraces from '../../EntityDetailsUtils/EntityTraces';
|
||||
import {
|
||||
getNamespaceMetricsQueryPayload,
|
||||
namespaceWidgetInfo,
|
||||
} from './constants';
|
||||
import { NamespaceDetailsProps } from './NamespaceDetails.interfaces';
|
||||
|
||||
import './NamespaceDetails.styles.scss';
|
||||
|
||||
function NamespaceDetails({
|
||||
namespace,
|
||||
onClose,
|
||||
isModalTimeSelection,
|
||||
}: NamespaceDetailsProps): 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_NAMESPACE_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_namespace_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: namespace?.namespaceName || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [
|
||||
namespace?.namespaceName,
|
||||
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: 'Namespace',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: namespace?.namespaceName || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [namespace?.namespaceName, eventsFiltersParam]);
|
||||
|
||||
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
|
||||
IBuilderQuery['filters']
|
||||
>(initialFilters);
|
||||
|
||||
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
|
||||
initialEventsFilters,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (namespace) {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Namespace,
|
||||
});
|
||||
}
|
||||
}, [namespace]);
|
||||
|
||||
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.Namespace,
|
||||
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.Namespace,
|
||||
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_NAMESPACE_NAME, 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_NAMESPACE_NAME,
|
||||
);
|
||||
|
||||
if (newFilters && newFilters?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Namespace,
|
||||
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_NAMESPACE_NAME, 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,
|
||||
category: InfraMonitoringEvents.Namespace,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(value?.items?.filter(
|
||||
(item) => item.key?.key !== QUERY_KEYS.K8S_NAMESPACE_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 namespaceKindFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
);
|
||||
const namespaceNameFilter = 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.Namespace,
|
||||
view: InfraMonitoringEvents.EventsView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: [
|
||||
namespaceKindFilter,
|
||||
namespaceNameFilter,
|
||||
...(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.Namespace,
|
||||
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">
|
||||
{namespace?.namespaceName}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!namespace}
|
||||
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 }} />}
|
||||
>
|
||||
{namespace && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Namespace Name
|
||||
</Typography.Text>
|
||||
<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={namespace.namespaceName}>
|
||||
{namespace.namespaceName}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title="Cluster name">
|
||||
{namespace.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 && (
|
||||
<NamespaceMetrics<K8sNamespacesData>
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
entity={namespace}
|
||||
entityWidgetInfo={namespaceWidgetInfo}
|
||||
getEntityQueryPayload={getNamespaceMetricsQueryPayload}
|
||||
category={K8sCategory.NAMESPACES}
|
||||
queryKey="namespaceMetrics"
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<NamespaceLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="namespaceLogs"
|
||||
category={K8sCategory.NAMESPACES}
|
||||
queryKeyFilters={[QUERY_KEYS.K8S_NAMESPACE_NAME]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<NamespaceTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||
tracesFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="namespaceTraces"
|
||||
category={InfraMonitoringEvents.Namespace}
|
||||
queryKeyFilters={[QUERY_KEYS.K8S_NAMESPACE_NAME]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.EVENTS && (
|
||||
<NamespaceEvents
|
||||
timeRange={modalTimeRange}
|
||||
handleChangeEventFilters={handleChangeEventsFilters}
|
||||
filters={eventsFilters}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
category={K8sCategory.NAMESPACES}
|
||||
queryKey="namespaceEvents"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default NamespaceDetails;
|
||||
@@ -1,67 +1,11 @@
|
||||
import { K8sNamespacesData } from 'api/infraMonitoring/getK8sNamespacesList';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
TagFilter,
|
||||
TagFilterItem,
|
||||
} 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 { K8sNamespacesData } from './api';
|
||||
|
||||
export const k8sNamespaceGetSelectedItemFilters = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_namespace_name',
|
||||
key: {
|
||||
key: 'k8s_namespace_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sNamespacesData>[] = [
|
||||
{ label: 'Namespace Name', getValue: (p): string => p.namespaceName },
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
getValue: (p): string => p.meta.k8s_cluster_name,
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sNamespaceInitialFilters = [
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
];
|
||||
|
||||
export const k8sNamespaceInitialEventsFilter = (
|
||||
item: K8sNamespacesData,
|
||||
): TagFilterItem[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Namespace'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.namespaceName),
|
||||
];
|
||||
|
||||
export const k8sNamespaceInitialLogTracesFilter = (
|
||||
item: K8sNamespacesData,
|
||||
): TagFilterItem[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.namespaceName),
|
||||
];
|
||||
|
||||
export const k8sNamespaceGetEntityName = (item: K8sNamespacesData): string =>
|
||||
item.namespaceName;
|
||||
|
||||
export const namespaceWidgetInfo = [
|
||||
{
|
||||
title: 'CPU Usage (cores)',
|
||||
@@ -0,0 +1,3 @@
|
||||
import NamespaceDetails from './NamespaceDetails';
|
||||
|
||||
export default NamespaceDetails;
|
||||
@@ -1,6 +0,0 @@
|
||||
.entityGroupHeader {
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -1,155 +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 { K8sNamespacesData, K8sNamespacesListPayload } from './api';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export interface K8sNamespacesRowData {
|
||||
key: string;
|
||||
itemKey: string;
|
||||
namespaceUID: string;
|
||||
namespaceName: React.ReactNode;
|
||||
clusterName: string;
|
||||
cpu: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
groupedByMeta?: Record<string, string>;
|
||||
}
|
||||
|
||||
export const k8sNamespacesColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Namespace Group',
|
||||
value: 'namespaceGroup',
|
||||
id: 'namespaceGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
value: 'clusterName',
|
||||
id: 'clusterName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Memory Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
|
||||
export const getK8sNamespacesListQuery = (): K8sNamespacesListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
export const k8sNamespacesColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> NAMESPACE GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'namespaceGroup',
|
||||
key: 'namespaceGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
},
|
||||
{
|
||||
title: <div>Namespace Name</div>,
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Cluster Name</div>,
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sNamespacesRenderRowData = (
|
||||
namespace: K8sNamespacesData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(
|
||||
namespace,
|
||||
() => namespace.namespaceName || namespace.meta.k8s_namespace_name,
|
||||
groupBy,
|
||||
),
|
||||
itemKey: namespace.meta.k8s_namespace_name,
|
||||
namespaceUID: namespace.namespaceName,
|
||||
namespaceName: (
|
||||
<Tooltip title={namespace.namespaceName}>
|
||||
{namespace.namespaceName || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
clusterName: namespace.meta.k8s_cluster_name,
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={namespace.cpuUsage}>
|
||||
{namespace.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={namespace.memoryUsage}>
|
||||
{formatBytes(namespace.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
namespaceGroup: getGroupByEl(namespace, groupBy),
|
||||
...namespace.meta,
|
||||
groupedByMeta: getGroupedByMeta(namespace, groupBy),
|
||||
});
|
||||
179
frontend/src/container/InfraMonitoringK8s/Namespaces/utils.tsx
Normal file
179
frontend/src/container/InfraMonitoringK8s/Namespaces/utils.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { TableColumnType as ColumnType, Tag } from 'antd';
|
||||
import {
|
||||
K8sNamespacesData,
|
||||
K8sNamespacesListPayload,
|
||||
} from 'api/infraMonitoring/getK8sNamespacesList';
|
||||
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: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
value: 'clusterName',
|
||||
id: 'clusterName',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Utilization (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Memory Utilization (bytes)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canRemove: false,
|
||||
},
|
||||
];
|
||||
|
||||
export interface K8sNamespacesRowData {
|
||||
key: string;
|
||||
namespaceUID: string;
|
||||
namespaceName: string;
|
||||
clusterName: string;
|
||||
cpu: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
groupedByMeta?: any;
|
||||
}
|
||||
|
||||
const namespaceGroupColumnConfig = {
|
||||
title: (
|
||||
<div className="column-header entity-group-header">
|
||||
<Group size={14} /> NAMESPACE GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'namespaceGroup',
|
||||
key: 'namespaceGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
className: 'column entity-group-header',
|
||||
};
|
||||
|
||||
export const getK8sNamespacesListQuery = (): K8sNamespacesListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
const columnsConfig = [
|
||||
{
|
||||
title: <div className="column-header-left">Namespace Name</div>,
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left">Cluster Name</div>,
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left">CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left">Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const getK8sNamespacesListColumns = (
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): ColumnType<K8sNamespacesRowData>[] => {
|
||||
if (groupBy.length > 0) {
|
||||
const filteredColumns = [...columnsConfig].filter(
|
||||
(column) => column.key !== 'namespaceName',
|
||||
);
|
||||
filteredColumns.unshift(namespaceGroupColumnConfig);
|
||||
return filteredColumns as ColumnType<K8sNamespacesRowData>[];
|
||||
}
|
||||
|
||||
return columnsConfig as ColumnType<K8sNamespacesRowData>[];
|
||||
};
|
||||
|
||||
const dotToUnder: Record<string, keyof K8sNamespacesData['meta']> = {
|
||||
'k8s.namespace.name': 'k8s_namespace_name',
|
||||
'k8s.cluster.name': 'k8s_cluster_name',
|
||||
};
|
||||
|
||||
const getGroupByEle = (
|
||||
namespace: K8sNamespacesData,
|
||||
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 namespace.meta;
|
||||
const value = namespace.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: K8sNamespacesData[],
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): K8sNamespacesRowData[] =>
|
||||
data.map((namespace, index) => ({
|
||||
key: index.toString(),
|
||||
namespaceUID: namespace.namespaceName,
|
||||
namespaceName: namespace.namespaceName,
|
||||
clusterName: namespace.meta.k8s_cluster_name,
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={namespace.cpuUsage}>
|
||||
{namespace.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={namespace.memoryUsage}>
|
||||
{formatBytes(namespace.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
namespaceGroup: getGroupByEle(namespace, groupBy),
|
||||
meta: namespace.meta,
|
||||
...namespace.meta,
|
||||
groupedByMeta: namespace.meta,
|
||||
}));
|
||||
@@ -0,0 +1,17 @@
|
||||
.infra-monitoring-container {
|
||||
.nodes-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,695 @@
|
||||
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 { K8sNodesListPayload } from 'api/infraMonitoring/getK8sNodesList';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useGetK8sNodesList } from 'hooks/infraMonitoring/useGetK8sNodesList';
|
||||
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 { getK8sNodesList, K8sNodeData } from './api';
|
||||
import {
|
||||
getNodeMetricsQueryPayload,
|
||||
k8sNodeDetailsMetadataConfig,
|
||||
k8sNodeGetEntityName,
|
||||
k8sNodeGetSelectedItemFilters,
|
||||
k8sNodeInitialEventsFilter,
|
||||
k8sNodeInitialFilters,
|
||||
k8sNodeInitialLogTracesFilter,
|
||||
nodeWidgetInfo,
|
||||
} from './constants';
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
K8sCategory,
|
||||
} from '../constants';
|
||||
import {
|
||||
k8sNodesColumns,
|
||||
k8sNodesColumnsConfig,
|
||||
k8sNodesRenderRowData,
|
||||
} from './table';
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringNodeUID,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
import K8sHeader from '../K8sHeader';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
import { usePageSize } from '../utils';
|
||||
import NodeDetails from './NodeDetails';
|
||||
import {
|
||||
defaultAddedColumns,
|
||||
formatDataForTable,
|
||||
getK8sNodesListColumns,
|
||||
getK8sNodesListQuery,
|
||||
K8sNodesRowData,
|
||||
} from './utils';
|
||||
|
||||
import '../InfraMonitoringK8s.styles.scss';
|
||||
import './K8sNodesList.styles.scss';
|
||||
|
||||
function K8sNodesList({
|
||||
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 [selectedNodeUID, setSelectedNodeUID] = useInfraMonitoringNodeUID();
|
||||
|
||||
const { pageSize, setPageSize } = usePageSize(K8sCategory.NODES);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
|
||||
// These params are used only for clearing in handleCloseNodeDetail
|
||||
const [, setView] = useInfraMonitoringView();
|
||||
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
|
||||
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
|
||||
const [, setLogFilters] = useInfraMonitoringLogFilters();
|
||||
|
||||
const [selectedRowData, setSelectedRowData] = useState<K8sNodesRowData | 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: K8sNodesRowData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): IBuilderQuery['filters'] => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...queryFilters.items],
|
||||
op: 'and',
|
||||
};
|
||||
|
||||
const response = await getK8sNodesList(
|
||||
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: K8sNodeData | null; error?: string | null }> => {
|
||||
const response = await getK8sNodesList(
|
||||
{
|
||||
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 = getK8sNodesListQuery();
|
||||
|
||||
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 (selectedNodeUID) {
|
||||
return [
|
||||
'nodeList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'nodeList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
queryFilters,
|
||||
orderBy,
|
||||
selectedNodeUID,
|
||||
minTime,
|
||||
maxTime,
|
||||
selectedRowData,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
isLoading: isLoadingGroupedByRowData,
|
||||
isError: isErrorGroupedByRowData,
|
||||
refetch: fetchGroupedByRowData,
|
||||
} = useGetK8sNodesList(
|
||||
fetchGroupedByRowDataQuery as K8sNodesListPayload,
|
||||
{
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: GetK8sEntityToAggregateAttribute(
|
||||
K8sCategory.NODES,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true,
|
||||
K8sCategory.NODES,
|
||||
);
|
||||
|
||||
const query = useMemo(() => {
|
||||
const baseQuery = getK8sNodesListQuery();
|
||||
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 nestedNodesData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) {
|
||||
return [];
|
||||
}
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const formattedGroupedByNodesData = useMemo(
|
||||
() =>
|
||||
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedNodeUID) {
|
||||
return [
|
||||
'nodeList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'nodeList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedNodeUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sNodesList(
|
||||
query as K8sNodesListPayload,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const nodesData = useMemo(() => data?.payload?.data?.records || [], [data]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const formattedNodesData = useMemo(
|
||||
() => formatDataForTable(nodesData, groupBy),
|
||||
[nodesData, groupBy],
|
||||
);
|
||||
|
||||
const columns = useMemo(() => getK8sNodesListColumns(groupBy), [groupBy]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sNodesRowData): 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<K8sNodesRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter: SorterResult<K8sNodesRowData> | SorterResult<K8sNodesRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Node,
|
||||
});
|
||||
}
|
||||
|
||||
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.Node,
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Node,
|
||||
total: data?.payload?.data?.total,
|
||||
});
|
||||
}, [data?.payload?.data?.total]);
|
||||
|
||||
const selectedNodeData = useMemo(() => {
|
||||
if (!selectedNodeUID) {
|
||||
return null;
|
||||
}
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the node from the formatted grouped by nodes data
|
||||
return (
|
||||
nestedNodesData.find((node) => node.nodeUID === selectedNodeUID) || null
|
||||
);
|
||||
}
|
||||
// If not grouped by, return the node from the nodes data
|
||||
return nodesData.find((node) => node.nodeUID === selectedNodeUID) || null;
|
||||
}, [selectedNodeUID, groupBy.length, nodesData, nestedNodesData]);
|
||||
|
||||
const openNodeInNewTab = (record: K8sNodesRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID, record.nodeUID);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sNodesRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openNodeInNewTab(record);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setSelectedNodeUID(record.nodeUID);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Node,
|
||||
});
|
||||
};
|
||||
|
||||
const nestedColumns = useMemo(() => getK8sNodesListColumns([]), []);
|
||||
|
||||
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
|
||||
className="expanded-table-view"
|
||||
columns={nestedColumns as ColumnType<K8sNodesRowData>[]}
|
||||
dataSource={formattedGroupedByNodesData}
|
||||
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 (isModifierKeyPressed(event)) {
|
||||
openNodeInNewTab(record);
|
||||
return;
|
||||
}
|
||||
setSelectedNodeUID(record.nodeUID);
|
||||
},
|
||||
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: K8sNodesRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sNodesRowData;
|
||||
}): 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 handleCloseNodeDetail = (): void => {
|
||||
setSelectedNodeUID(null);
|
||||
setView(null);
|
||||
setTracesFilters(null);
|
||||
setEventsFilters(null);
|
||||
setLogFilters(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(
|
||||
(k) => k.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.Node,
|
||||
});
|
||||
},
|
||||
[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.Node,
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedNodesData.length === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<K8sNodeData>
|
||||
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.NODES}
|
||||
tableColumnsDefinitions={k8sNodesColumns}
|
||||
tableColumns={k8sNodesColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sNodesRenderRowData}
|
||||
eventCategory={InfraMonitoringEvents.Node}
|
||||
showAutoRefresh={!selectedNodeData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
<Table
|
||||
className="k8s-list-table nodes-list-table"
|
||||
dataSource={showTableLoadingState ? [] : formattedNodesData}
|
||||
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<K8sNodeData>
|
||||
category={K8sCategory.NODES}
|
||||
eventCategory={InfraMonitoringEvents.Node}
|
||||
getSelectedItemFilters={k8sNodeGetSelectedItemFilters}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sNodeGetEntityName}
|
||||
getInitialLogTracesFilters={k8sNodeInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sNodeInitialEventsFilter}
|
||||
primaryFilterKeys={k8sNodeInitialFilters}
|
||||
metadataConfig={k8sNodeDetailsMetadataConfig}
|
||||
entityWidgetInfo={nodeWidgetInfo}
|
||||
getEntityQueryPayload={getNodeMetricsQueryPayload}
|
||||
queryKeyPrefix="node"
|
||||
<NodeDetails
|
||||
node={selectedNodeData}
|
||||
isModalTimeSelection
|
||||
onClose={handleCloseNodeDetail}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { K8sNodesData } from 'api/infraMonitoring/getK8sNodesList';
|
||||
|
||||
export type NodeDetailsProps = {
|
||||
node: K8sNodesData | null;
|
||||
isModalTimeSelection: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -0,0 +1,635 @@
|
||||
/* 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 { K8sNodesData } from 'api/infraMonitoring/getK8sNodesList';
|
||||
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 NodeEvents from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityEvents';
|
||||
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 NodeLogs from '../../EntityDetailsUtils/EntityLogs';
|
||||
import NodeMetrics from '../../EntityDetailsUtils/EntityMetrics';
|
||||
import NodeTraces from '../../EntityDetailsUtils/EntityTraces';
|
||||
import { QUERY_KEYS } from '../../EntityDetailsUtils/utils';
|
||||
import { getNodeMetricsQueryPayload, nodeWidgetInfo } from './constants';
|
||||
import { NodeDetailsProps } from './NodeDetails.interfaces';
|
||||
|
||||
import '../../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
|
||||
function NodeDetails({
|
||||
node,
|
||||
onClose,
|
||||
isModalTimeSelection,
|
||||
}: NodeDetailsProps): 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_NODE_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_node_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: node?.meta.k8s_node_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [
|
||||
node?.meta.k8s_node_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: 'Node',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: node?.meta.k8s_node_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [node?.meta.k8s_node_name, eventsFiltersParam]);
|
||||
|
||||
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
|
||||
IBuilderQuery['filters']
|
||||
>(initialFilters);
|
||||
|
||||
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
|
||||
initialEventsFilters,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (node) {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Node,
|
||||
});
|
||||
}
|
||||
}, [node]);
|
||||
|
||||
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.Node,
|
||||
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.Node,
|
||||
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_NODE_NAME, 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_NODE_NAME,
|
||||
);
|
||||
|
||||
if (newFilters && newFilters?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Node,
|
||||
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_NODE_NAME, 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,
|
||||
category: InfraMonitoringEvents.Node,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(value?.items?.filter(
|
||||
(item) => item.key?.key !== QUERY_KEYS.K8S_NODE_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 nodeKindFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
);
|
||||
const nodeNameFilter = 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.Node,
|
||||
view: InfraMonitoringEvents.EventsView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: [
|
||||
nodeKindFilter,
|
||||
nodeNameFilter,
|
||||
...(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.Node,
|
||||
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">
|
||||
{node?.meta.k8s_node_name}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!node}
|
||||
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 }} />}
|
||||
>
|
||||
{node && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Node Name
|
||||
</Typography.Text>
|
||||
<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={node.meta.k8s_node_name}>
|
||||
{node.meta.k8s_node_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title="Cluster name">{node.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 && (
|
||||
<NodeMetrics<K8sNodesData>
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
entity={node}
|
||||
entityWidgetInfo={nodeWidgetInfo}
|
||||
getEntityQueryPayload={getNodeMetricsQueryPayload}
|
||||
category={K8sCategory.NODES}
|
||||
queryKey="nodeMetrics"
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<NodeLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKeyFilters={[QUERY_KEYS.K8S_NODE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME]}
|
||||
queryKey="nodeLogs"
|
||||
category={K8sCategory.NODES}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<NodeTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||
tracesFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKeyFilters={[QUERY_KEYS.K8S_NODE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME]}
|
||||
queryKey="nodeTraces"
|
||||
category={InfraMonitoringEvents.Node}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.EVENTS && (
|
||||
<NodeEvents
|
||||
timeRange={modalTimeRange}
|
||||
handleChangeEventFilters={handleChangeEventsFilters}
|
||||
filters={eventsFilters}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
category={K8sCategory.NODES}
|
||||
queryKey="nodeEvents"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default NodeDetails;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user