mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-05 12:40:46 +01:00
Compare commits
15 Commits
feat/alert
...
hotfix/pip
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
decf6760d3 | ||
|
|
1e20dc7a87 | ||
|
|
a6ac14344e | ||
|
|
e4e1c4b9e0 | ||
|
|
816ae7760e | ||
|
|
985523df93 | ||
|
|
5c9606bf9b | ||
|
|
f9461415bb | ||
|
|
d93713c184 | ||
|
|
0b5d3e939f | ||
|
|
089f9eb4c6 | ||
|
|
6c659e3b26 | ||
|
|
c70866e4b3 | ||
|
|
34041be308 | ||
|
|
957580ec7c |
3
.github/workflows/integrationci.yaml
vendored
3
.github/workflows/integrationci.yaml
vendored
@@ -39,6 +39,8 @@ jobs:
|
||||
matrix:
|
||||
suite:
|
||||
- alerts
|
||||
- alertmanager
|
||||
- alertmanagerrotation
|
||||
- basepath
|
||||
- callbackauthn
|
||||
- cloudintegrations
|
||||
@@ -53,6 +55,7 @@ jobs:
|
||||
- queriermetrics
|
||||
- querierscalar
|
||||
- queriercommon
|
||||
- querierai
|
||||
- rawexportdata
|
||||
- promqlconformance
|
||||
- querierauthz
|
||||
|
||||
@@ -6902,6 +6902,7 @@ components:
|
||||
Querybuildertypesv5QueryEnvelope:
|
||||
discriminator:
|
||||
mapping:
|
||||
builder_ai_query: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilderAI'
|
||||
builder_formula: '#/components/schemas/Querybuildertypesv5QueryEnvelopeFormula'
|
||||
builder_query: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilder'
|
||||
builder_trace_operator: '#/components/schemas/Querybuildertypesv5QueryEnvelopeTraceOperator'
|
||||
@@ -6910,6 +6911,7 @@ components:
|
||||
propertyName: type
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilder'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilderAI'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeFormula'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeTraceOperator'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopePromQL'
|
||||
@@ -6924,6 +6926,15 @@ components:
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
Querybuildertypesv5QueryEnvelopeBuilderAI:
|
||||
properties:
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation'
|
||||
type:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
Querybuildertypesv5QueryEnvelopeClickHouseSQL:
|
||||
properties:
|
||||
spec:
|
||||
@@ -7037,6 +7048,7 @@ components:
|
||||
Querybuildertypesv5QueryType:
|
||||
enum:
|
||||
- builder_query
|
||||
- builder_ai_query
|
||||
- builder_formula
|
||||
- builder_trace_operator
|
||||
- clickhouse_sql
|
||||
@@ -15477,6 +15489,72 @@ paths:
|
||||
summary: Lock dashboard (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/dashboards/{id}/migrate:
|
||||
post:
|
||||
deprecated: false
|
||||
description: 'This endpoint retries the v1→v2 (Perses) migration on a dashboard
|
||||
still stored in the v1 schema and returns the v2-shape result. It is idempotent:
|
||||
a dashboard already in the v2 schema is returned unchanged.'
|
||||
operationId: MigrateDashboardV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesGettableDashboardV2'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Migrate dashboard to v2
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/factor_password/forgot:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -232,14 +232,11 @@ cd tests/e2e
|
||||
# Single feature dir
|
||||
npx playwright test tests/alerts/ --project=chromium
|
||||
|
||||
# Single sub-area
|
||||
npx playwright test tests/alerts/history/ --project=chromium
|
||||
|
||||
# Single file
|
||||
npx playwright test tests/alerts/page.spec.ts --project=chromium
|
||||
npx playwright test tests/alerts/alerts.spec.ts --project=chromium
|
||||
|
||||
# Single test by title grep
|
||||
npx playwright test --project=chromium -g "AL-01"
|
||||
npx playwright test --project=chromium -g "TC-01"
|
||||
```
|
||||
|
||||
### Iterative modes
|
||||
@@ -273,14 +270,7 @@ yarn test:staging
|
||||
| `SIGNOZ_E2E_PASSWORD` | Admin password. Bootstrap writes the integration-test default. |
|
||||
| `SIGNOZ_E2E_SEEDER_URL` | Seeder HTTP base URL — hit by specs that need per-test telemetry. |
|
||||
|
||||
Precedence in `playwright.config.ts`, lowest to highest: `.env` (user-provided, staging) → `.env.local` (bootstrap-generated, local mode) → whatever is already in `process.env`. The config parses both files itself and only fills in keys the environment does not already define, so exporting a variable always wins:
|
||||
|
||||
```bash
|
||||
# runs against a locally served frontend, not whatever .env.local points at
|
||||
SIGNOZ_E2E_BASE_URL=http://127.0.0.1:3301 pnpm test tests/alerts
|
||||
```
|
||||
|
||||
This is deliberately not `dotenv.config({ override: true })`. That flag makes the *file* beat `process.env`, which silently discarded exported values — including the `SIGNOZ_E2E_BASE_URL` in `pnpm test:staging`, whenever a `.env.local` happened to exist.
|
||||
Loading order in `playwright.config.ts`: `.env` first (user-provided, staging), then `.env.local` with `override: true` (bootstrap-generated, local mode). Anything already set in `process.env` at yarn-test time wins because dotenv doesn't touch vars that are already present.
|
||||
|
||||
### Playwright options
|
||||
|
||||
|
||||
@@ -276,6 +276,10 @@ func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UU
|
||||
return module.pkgDashboardModule.GetV2(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (module *module) MigrateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
|
||||
return module.pkgDashboardModule.MigrateV2(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatable dashboardtypes.UpdatableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
return module.pkgDashboardModule.UpdateV2(ctx, orgID, id, updatedBy, updatable)
|
||||
}
|
||||
|
||||
@@ -80,15 +80,6 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
Route: "",
|
||||
})
|
||||
|
||||
fineGrainedAuthz := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseFineGrainedAuthz, evalCtx)
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureUseFineGrainedAuthz.String()),
|
||||
Active: fineGrainedAuthz,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
})
|
||||
|
||||
aiObservability := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureEnableAIObservability, evalCtx)
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureEnableAIObservability.String()),
|
||||
@@ -107,15 +98,6 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
Route: "",
|
||||
})
|
||||
|
||||
infraMonitoringV2 := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseInfraMonitoringV2, evalCtx)
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureUseInfraMonitoringV2.String()),
|
||||
Active: infraMonitoringV2,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
})
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.DotMetricsEnabled {
|
||||
|
||||
@@ -52,6 +52,8 @@ import type {
|
||||
ListDashboardsV2200,
|
||||
ListDashboardsV2Params,
|
||||
LockDashboardV2PathParameters,
|
||||
MigrateDashboardV2200,
|
||||
MigrateDashboardV2PathParameters,
|
||||
PatchDashboardV2200,
|
||||
PatchDashboardV2PathParameters,
|
||||
PinDashboardV2PathParameters,
|
||||
@@ -1804,6 +1806,85 @@ export const useLockDashboardV2 = <
|
||||
> => {
|
||||
return useMutation(getLockDashboardV2MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint retries the v1→v2 (Perses) migration on a dashboard still stored in the v1 schema and returns the v2-shape result. It is idempotent: a dashboard already in the v2 schema is returned unchanged.
|
||||
* @summary Migrate dashboard to v2
|
||||
*/
|
||||
export const migrateDashboardV2 = (
|
||||
{ id }: MigrateDashboardV2PathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<MigrateDashboardV2200>({
|
||||
url: `/api/v2/dashboards/${id}/migrate`,
|
||||
method: 'POST',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getMigrateDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof migrateDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: MigrateDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof migrateDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: MigrateDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['migrateDashboardV2'];
|
||||
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 migrateDashboardV2>>,
|
||||
{ pathParams: MigrateDashboardV2PathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return migrateDashboardV2(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type MigrateDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof migrateDashboardV2>>
|
||||
>;
|
||||
|
||||
export type MigrateDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Migrate dashboard to v2
|
||||
*/
|
||||
export const useMigrateDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof migrateDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: MigrateDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof migrateDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: MigrateDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getMigrateDashboardV2MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint returns the sanitized v2-shape dashboard data for public access. Each panel query is reduced to a safe field subset, so filters and raw query strings are not exposed.
|
||||
* @summary Get public dashboard data (v2)
|
||||
|
||||
@@ -4301,6 +4301,18 @@ export interface Querybuildertypesv5QueryEnvelopeBuilderDTO {
|
||||
type: Querybuildertypesv5QueryEnvelopeBuilderDTOType;
|
||||
}
|
||||
|
||||
export enum Querybuildertypesv5QueryEnvelopeBuilderAIDTOType {
|
||||
builder_ai_query = 'builder_ai_query',
|
||||
}
|
||||
export interface Querybuildertypesv5QueryEnvelopeBuilderAIDTO {
|
||||
spec?: Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregationDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @enum builder_ai_query
|
||||
*/
|
||||
type: Querybuildertypesv5QueryEnvelopeBuilderAIDTOType;
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5QueryBuilderFormulaDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
@@ -4484,6 +4496,7 @@ export interface Querybuildertypesv5QueryEnvelopeClickHouseSQLDTO {
|
||||
|
||||
export type Querybuildertypesv5QueryEnvelopeDTO =
|
||||
| Querybuildertypesv5QueryEnvelopeBuilderDTO
|
||||
| Querybuildertypesv5QueryEnvelopeBuilderAIDTO
|
||||
| Querybuildertypesv5QueryEnvelopeFormulaDTO
|
||||
| Querybuildertypesv5QueryEnvelopeTraceOperatorDTO
|
||||
| Querybuildertypesv5QueryEnvelopePromQLDTO
|
||||
@@ -8287,6 +8300,7 @@ export interface Querybuildertypesv5QueryRangeResponseDTO {
|
||||
|
||||
export enum Querybuildertypesv5QueryTypeDTO {
|
||||
builder_query = 'builder_query',
|
||||
builder_ai_query = 'builder_ai_query',
|
||||
builder_formula = 'builder_formula',
|
||||
builder_trace_operator = 'builder_trace_operator',
|
||||
clickhouse_sql = 'clickhouse_sql',
|
||||
@@ -11164,6 +11178,17 @@ export type UnlockDashboardV2PathParameters = {
|
||||
export type LockDashboardV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type MigrateDashboardV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type MigrateDashboardV2200 = {
|
||||
data: DashboardtypesGettableDashboardV2DTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetFeatures200 = {
|
||||
/**
|
||||
* @type array
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import type { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { getNavigationReferrer } from 'lib/navigation';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { isCustomTimeRange } from 'store/globalTime';
|
||||
|
||||
export enum Events {
|
||||
UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE',
|
||||
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
|
||||
@@ -45,155 +39,3 @@ export enum InfraMonitoringEvents {
|
||||
StatefulSet = 'statefulSet',
|
||||
Volumes = 'volumes',
|
||||
}
|
||||
|
||||
export function logInfraFilterCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
source: 'quick_filter' | 'search' | 'host_status_toggle',
|
||||
expression: string,
|
||||
extraKeys?: string[],
|
||||
): void {
|
||||
const expressionKeys = extractQueryPairs(expression?.trim() || '').map(
|
||||
(pair) => pair.key,
|
||||
);
|
||||
|
||||
if (extraKeys) {
|
||||
extraKeys.forEach((key) => expressionKeys.push(key));
|
||||
}
|
||||
|
||||
if (expressionKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void logEvent('infra_filter_customized', {
|
||||
entity_type: entityType,
|
||||
source,
|
||||
expression_keys: [...new Set(expressionKeys)],
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraMonitoringListViewedEvent(
|
||||
entity: InfraMonitoringEntity,
|
||||
): void {
|
||||
const referrer = getNavigationReferrer();
|
||||
|
||||
void logEvent('infra_list_viewed', {
|
||||
entity,
|
||||
referrer,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraTimeRangeCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
rangeLabel: string,
|
||||
): void {
|
||||
void logEvent('infra_time_range_customized', {
|
||||
entity_type: entityType,
|
||||
range_label: isCustomTimeRange(rangeLabel) ? 'custom' : rangeLabel,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraColumnCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
columnsList: string[],
|
||||
fontSize: string,
|
||||
maxLinesPerRow: number,
|
||||
source: 'list' | 'expanded',
|
||||
): void {
|
||||
void logEvent('infra_column_customized', {
|
||||
entity_type: entityType,
|
||||
columns_list: columnsList,
|
||||
font_size: fontSize,
|
||||
max_lines_per_row: maxLinesPerRow,
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraColumnSortedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
columnKey: string,
|
||||
direction: 'asc' | 'desc',
|
||||
source: 'list' | 'expanded',
|
||||
): void {
|
||||
void logEvent('infra_column_sorted', {
|
||||
entity_type: entityType,
|
||||
column_key: columnKey,
|
||||
direction,
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraDrawerTimeRangeCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
rangeLabel: string,
|
||||
): void {
|
||||
void logEvent('infra_drawer_time_range_customized', {
|
||||
entity_type: entityType,
|
||||
range_label: isCustomTimeRange(rangeLabel) ? 'custom' : rangeLabel,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraDrawerFilterCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
tab: 'metrics' | 'logs' | 'traces' | 'events' | 'pod_metrics',
|
||||
expression: string,
|
||||
filterSource: 'search' | 'logs',
|
||||
): void {
|
||||
const expressionKeys = extractQueryPairs(expression?.trim() || '').map(
|
||||
(pair) => pair.key,
|
||||
);
|
||||
|
||||
if (expressionKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void logEvent('infra_drawer_filter_customized', {
|
||||
entity_type: entityType,
|
||||
tab,
|
||||
expression_keys: [...new Set(expressionKeys)],
|
||||
filter_source: filterSource,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraGroupByCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
groupByKeysList: string[],
|
||||
): void {
|
||||
void logEvent('infra_group_by_customized', {
|
||||
entity_type: entityType,
|
||||
group_by_keys_list: groupByKeysList,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraDrawerTabViewedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
tab: string,
|
||||
isDefaultTab: boolean,
|
||||
): void {
|
||||
void logEvent('infra_drawer_tab_viewed', {
|
||||
entity_type: entityType,
|
||||
tab,
|
||||
is_default_tab: isDefaultTab,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraExplorerNavigatedEvent(params: {
|
||||
entityType: InfraMonitoringEntity;
|
||||
destination:
|
||||
| 'metrics_explorer'
|
||||
| 'logs_explorer'
|
||||
| 'traces_explorer'
|
||||
| 'k8s_list';
|
||||
source: 'chart_compass_icon' | 'tab_cta_button' | 'stats_card';
|
||||
tab: string;
|
||||
sourceKey: string | null;
|
||||
drawerDurationMsAtNavigation: number | null;
|
||||
}): void {
|
||||
void logEvent('infra_explorer_navigated', {
|
||||
entity_type: params.entityType,
|
||||
destination: params.destination,
|
||||
source: params.source,
|
||||
tab: params.tab,
|
||||
source_key: params.sourceKey,
|
||||
drawer_duration_ms_at_navigation: params.drawerDurationMsAtNavigation,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ export enum FeatureKeys {
|
||||
ANOMALY_DETECTION = 'anomaly_detection',
|
||||
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
|
||||
USE_JSON_BODY = 'use_json_body',
|
||||
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
|
||||
USE_INFRA_MONITORING_V2 = 'use_infra_monitoring_v2',
|
||||
ENABLE_AI_OBSERVABILITY = 'enable_ai_observability',
|
||||
ENABLE_METRICS_REDUCTION = 'enable_metrics_reduction',
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ function PopoverContent({
|
||||
<Link
|
||||
to={`${ROUTES.LOGS_EXPLORER}?${relatedLogsLink}`}
|
||||
className="contributor-row-popover-buttons__button"
|
||||
data-testid="alert-popover-view-logs"
|
||||
>
|
||||
<div className="icon">
|
||||
<LogsIcon />
|
||||
@@ -41,7 +40,6 @@ function PopoverContent({
|
||||
<Link
|
||||
to={`${ROUTES.TRACES_EXPLORER}?${relatedTracesLink}`}
|
||||
className="contributor-row-popover-buttons__button"
|
||||
data-testid="alert-popover-view-traces"
|
||||
>
|
||||
<div className="icon">
|
||||
<DraftingCompass
|
||||
|
||||
@@ -26,10 +26,7 @@ function ChangePercentage({
|
||||
}: ChangePercentageProps): JSX.Element {
|
||||
if (direction > 0) {
|
||||
return (
|
||||
<div
|
||||
className="change-percentage change-percentage--success"
|
||||
data-testid="stats-card-change"
|
||||
>
|
||||
<div className="change-percentage change-percentage--success">
|
||||
<div className="change-percentage__icon">
|
||||
<ArrowDownLeft size={14} color={Color.BG_FOREST_500} />
|
||||
</div>
|
||||
@@ -41,10 +38,7 @@ function ChangePercentage({
|
||||
}
|
||||
if (direction < 0) {
|
||||
return (
|
||||
<div
|
||||
className="change-percentage change-percentage--error"
|
||||
data-testid="stats-card-change"
|
||||
>
|
||||
<div className="change-percentage change-percentage--error">
|
||||
<div className="change-percentage__icon">
|
||||
<ArrowUpRight size={14} color={Color.BG_CHERRY_500} />
|
||||
</div>
|
||||
@@ -56,10 +50,7 @@ function ChangePercentage({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="change-percentage change-percentage--no-previous-data"
|
||||
data-testid="stats-card-change"
|
||||
>
|
||||
<div className="change-percentage change-percentage--no-previous-data">
|
||||
<div className="change-percentage__label">no previous data</div>
|
||||
</div>
|
||||
);
|
||||
@@ -112,12 +103,7 @@ function StatsCard({
|
||||
const formattedEndTimeForTooltip = convertTimestampToLocaleDateString(endTime);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`stats-card ${isEmpty ? 'stats-card--empty' : ''}`}
|
||||
data-testid="stats-card"
|
||||
data-stats-title={title}
|
||||
data-empty={isEmpty ? 'true' : 'false'}
|
||||
>
|
||||
<div className={`stats-card ${isEmpty ? 'stats-card--empty' : ''}`}>
|
||||
<div className="stats-card__title-wrapper">
|
||||
<div className="title">{title}</div>
|
||||
<div className="duration-indicator">
|
||||
@@ -137,7 +123,7 @@ function StatsCard({
|
||||
</div>
|
||||
|
||||
<div className="stats-card__stats">
|
||||
<div className="count-label" data-testid="stats-card-value">
|
||||
<div className="count-label">
|
||||
{isEmpty ? emptyMessage : displayValue || totalCurrentCount}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -81,11 +81,7 @@ function StatsGraph({ timeSeries, changeDirection }: Props): JSX.Element {
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
ref={graphRef}
|
||||
data-testid="stats-card-sparkline"
|
||||
>
|
||||
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
|
||||
<Uplot data={[xData, yData]} options={options} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -48,16 +48,11 @@ function TopContributorsCard({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="top-contributors-card" data-testid="top-contributors-card">
|
||||
<div className="top-contributors-card">
|
||||
<div className="top-contributors-card__header">
|
||||
<div className="title">top contributors</div>
|
||||
{topContributorsData.length > 3 && (
|
||||
<Button
|
||||
type="text"
|
||||
className="view-all"
|
||||
onClick={toggleViewAllDrawer}
|
||||
data-testid="top-contributors-view-all"
|
||||
>
|
||||
<Button type="text" className="view-all" onClick={toggleViewAllDrawer}>
|
||||
<div className="label">View all</div>
|
||||
<div className="icon">
|
||||
<ArrowRight
|
||||
|
||||
@@ -68,10 +68,7 @@ function TopContributorsRows({
|
||||
relatedTracesLink={record.relatedTracesLink}
|
||||
relatedLogsLink={record.relatedLogsLink}
|
||||
>
|
||||
<div
|
||||
className="total-contribution"
|
||||
data-testid="top-contributors-row-count"
|
||||
>
|
||||
<div className="total-contribution">
|
||||
{count}/{totalCurrentTriggers}
|
||||
</div>
|
||||
</ConditionalAlertPopover>
|
||||
@@ -81,10 +78,7 @@ function TopContributorsRows({
|
||||
|
||||
const handleRowClick = (
|
||||
record: AlertRuleTopContributors,
|
||||
): HTMLAttributes<AlertRuleTimelineTableResponse> & {
|
||||
'data-testid': string;
|
||||
} => ({
|
||||
'data-testid': 'top-contributors-row',
|
||||
): HTMLAttributes<AlertRuleTimelineTableResponse> => ({
|
||||
onClick: (): void => {
|
||||
logEvent('Alert history: Top contributors row: Clicked', {
|
||||
labels: record.labels,
|
||||
|
||||
@@ -31,10 +31,7 @@ function ViewAllDrawer({
|
||||
}}
|
||||
title="Viewing All Contributors"
|
||||
>
|
||||
<div
|
||||
className="top-contributors-card--view-all"
|
||||
data-testid="top-contributors-drawer"
|
||||
>
|
||||
<div className="top-contributors-card--view-all">
|
||||
<div className="top-contributors-card__content">
|
||||
<TopContributorsRows
|
||||
topContributors={topContributorsData}
|
||||
|
||||
@@ -32,8 +32,8 @@ function GraphWrapper({
|
||||
}, [data?.data]);
|
||||
|
||||
return (
|
||||
<div className="timeline-graph" data-testid="timeline-graph">
|
||||
<div className="timeline-graph__title" data-testid="timeline-graph-title">
|
||||
<div className="timeline-graph">
|
||||
<div className="timeline-graph__title">
|
||||
{totalCurrentTriggers} triggers in {relativeTime}
|
||||
</div>
|
||||
<div className="timeline-graph__chart">
|
||||
|
||||
@@ -118,10 +118,7 @@ function TimelineTableContent(): JSX.Element {
|
||||
|
||||
const handleRowClick = (
|
||||
record: AlertRuleTimelineTableResponse,
|
||||
): HTMLAttributes<AlertRuleTimelineTableResponse> & {
|
||||
'data-testid': string;
|
||||
} => ({
|
||||
'data-testid': 'timeline-row',
|
||||
): HTMLAttributes<AlertRuleTimelineTableResponse> => ({
|
||||
onClick: (): void => {
|
||||
void logEvent('Alert history: Timeline table row: Clicked', {
|
||||
ruleId: record.ruleID,
|
||||
@@ -131,15 +128,12 @@ function TimelineTableContent(): JSX.Element {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="timeline-table" data-testid="timeline-table">
|
||||
<div className="timeline-table">
|
||||
{/* If we don't wait to have the keys, the QuerySearch will not render them at first usage */}
|
||||
{!isLoadingKeys && hardcodedAttributeKeys ? (
|
||||
<div className="timeline-table__filter">
|
||||
<div className="timeline-table__filter-row">
|
||||
<div
|
||||
className="timeline-table__filter-search"
|
||||
data-testid="timeline-filter-search"
|
||||
>
|
||||
<div className="timeline-table__filter-search">
|
||||
<QuerySearch
|
||||
onChange={querySearchOnChange}
|
||||
queryData={queryData}
|
||||
@@ -161,7 +155,6 @@ function TimelineTableContent(): JSX.Element {
|
||||
<Skeleton.Input
|
||||
className="timeline-table__filter--loading-skeleton"
|
||||
active
|
||||
data-testid="timeline-filter-skeleton"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -179,17 +172,14 @@ function TimelineTableContent(): JSX.Element {
|
||||
locale={{
|
||||
emptyText:
|
||||
isError && apiError ? (
|
||||
<div className="timeline-table__error" data-testid="timeline-error">
|
||||
<div className="timeline-table__error">
|
||||
<ErrorContent error={apiError} />
|
||||
</div>
|
||||
) : undefined,
|
||||
}}
|
||||
footer={(): JSX.Element => (
|
||||
<div className="timeline-table__pagination">
|
||||
<div
|
||||
className="timeline-table__pagination-info"
|
||||
data-testid="timeline-footer-range"
|
||||
>
|
||||
<div className="timeline-table__pagination-info">
|
||||
{paginationConfig.showTotal?.(totalItems, [
|
||||
totalItems === 0
|
||||
? 0
|
||||
|
||||
@@ -21,7 +21,7 @@ export const timelineTableColumns = ({
|
||||
sorter: true,
|
||||
width: 140,
|
||||
render: (value): JSX.Element => (
|
||||
<div className="alert-rule-state" data-testid="timeline-row-state">
|
||||
<div className="alert-rule-state">
|
||||
<AlertState state={value} showLabel />
|
||||
</div>
|
||||
),
|
||||
@@ -30,7 +30,7 @@ export const timelineTableColumns = ({
|
||||
title: 'LABELS',
|
||||
dataIndex: 'labels',
|
||||
render: (labels): JSX.Element => (
|
||||
<div className="alert-rule-labels" data-testid="timeline-row-labels">
|
||||
<div className="alert-rule-labels">
|
||||
<AlertLabels labels={labels} />
|
||||
</div>
|
||||
),
|
||||
@@ -40,10 +40,7 @@ export const timelineTableColumns = ({
|
||||
dataIndex: 'unixMilli',
|
||||
width: 200,
|
||||
render: (value): JSX.Element => (
|
||||
<div
|
||||
className="alert-rule__created-at"
|
||||
data-testid="timeline-row-created-at"
|
||||
>
|
||||
<div className="alert-rule__created-at">
|
||||
{formatTimezoneAdjustedTimestamp(value, DATE_TIME_FORMATS.DASH_DATETIME)}
|
||||
</div>
|
||||
),
|
||||
@@ -56,7 +53,7 @@ export const timelineTableColumns = ({
|
||||
if (!record.relatedTracesLink && !record.relatedLogsLink) {
|
||||
return (
|
||||
<Tooltip title="No links available for this item">
|
||||
<Button type="text" ghost disabled data-testid="timeline-row-actions">
|
||||
<Button type="text" ghost disabled>
|
||||
<Ellipsis className="dropdown-icon" size="md" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -68,7 +65,7 @@ export const timelineTableColumns = ({
|
||||
relatedTracesLink={record.relatedTracesLink ?? ''}
|
||||
relatedLogsLink={record.relatedLogsLink ?? ''}
|
||||
>
|
||||
<Button type="text" ghost data-testid="timeline-row-actions">
|
||||
<Button type="text" ghost>
|
||||
<Ellipsis className="dropdown-icon" size="md" />
|
||||
</Button>
|
||||
</ConditionalAlertPopover>
|
||||
|
||||
@@ -23,7 +23,6 @@ function TimelineTabs(): JSX.Element {
|
||||
{
|
||||
value: TimelineTab.OVERALL_STATUS,
|
||||
label: 'Overall Status',
|
||||
testId: 'timeline-tab-overall-status',
|
||||
},
|
||||
{
|
||||
value: TimelineTab.TOP_5_CONTRIBUTORS,
|
||||
@@ -34,7 +33,6 @@ function TimelineTabs(): JSX.Element {
|
||||
</div>
|
||||
),
|
||||
disabled: true,
|
||||
testId: 'timeline-tab-top-contributors',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -59,17 +57,14 @@ function TimelineFilters(): JSX.Element {
|
||||
{
|
||||
value: TimelineFilter.ALL,
|
||||
label: 'All',
|
||||
testId: 'timeline-filter-all',
|
||||
},
|
||||
{
|
||||
value: TimelineFilter.FIRED,
|
||||
label: 'Fired',
|
||||
testId: 'timeline-filter-fired',
|
||||
},
|
||||
{
|
||||
value: TimelineFilter.RESOLVED,
|
||||
label: 'Resolved',
|
||||
testId: 'timeline-filter-resolved',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ function AdvancedOptions(): JSX.Element {
|
||||
})
|
||||
}
|
||||
value={advancedOptions.sendNotificationIfDataIsMissing.toleranceLimit}
|
||||
testId="send-notification-if-data-is-missing-input"
|
||||
/>
|
||||
<Typography.Text>Minutes</Typography.Text>
|
||||
</div>
|
||||
@@ -67,7 +66,6 @@ function AdvancedOptions(): JSX.Element {
|
||||
})
|
||||
}
|
||||
value={advancedOptions.enforceMinimumDatapoints.minimumDatapoints}
|
||||
testId="enforce-minimum-datapoints-input"
|
||||
/>
|
||||
<Typography.Text>Datapoints</Typography.Text>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,6 @@ function EvaluationWindowPopover({
|
||||
tabIndex={0}
|
||||
data-value={option.value}
|
||||
data-section-id={sectionId}
|
||||
data-testid={`${sectionId}-option-${option.value}`}
|
||||
onClick={(): void => onChange(option.value)}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
|
||||
@@ -186,7 +186,6 @@ function Footer(): JSX.Element {
|
||||
color="primary"
|
||||
onClick={handleSaveAlert}
|
||||
disabled={disableButtons || Boolean(alertValidationMessage)}
|
||||
testId="save-alert-rule-button"
|
||||
>
|
||||
{isCreatingAlertRule || isUpdatingAlertRule ? (
|
||||
<Loader data-testid="save-alert-rule-loader-icon" size={14} />
|
||||
@@ -219,7 +218,6 @@ function Footer(): JSX.Element {
|
||||
color="secondary"
|
||||
onClick={handleTestNotification}
|
||||
disabled={disableButtons || Boolean(alertValidationMessage)}
|
||||
testId="test-notification-button"
|
||||
>
|
||||
{isTestingAlertRule ? (
|
||||
<Loader data-testid="test-notification-loader-icon" size={14} />
|
||||
@@ -251,7 +249,6 @@ function Footer(): JSX.Element {
|
||||
color="secondary"
|
||||
onClick={handleDiscard}
|
||||
disabled={disableButtons}
|
||||
testId="discard-alert-rule-button"
|
||||
>
|
||||
<X size={14} /> Discard
|
||||
</Button>
|
||||
|
||||
@@ -119,7 +119,6 @@ function BasicInfo({
|
||||
<SeveritySelect
|
||||
getPopupContainer={popupContainer}
|
||||
defaultValue="critical"
|
||||
data-testid="alert-severity-select"
|
||||
onChange={(value: unknown | string): void => {
|
||||
const s = (value as string) || 'critical';
|
||||
setAlertDef({
|
||||
@@ -148,7 +147,6 @@ function BasicInfo({
|
||||
]}
|
||||
>
|
||||
<InputSmall
|
||||
data-testid="alert-name-input-v1"
|
||||
onChange={(e): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
@@ -163,7 +161,6 @@ function BasicInfo({
|
||||
name={['annotations', 'description']}
|
||||
>
|
||||
<TextareaMedium
|
||||
data-testid="alert-description-input"
|
||||
onChange={(e): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
|
||||
@@ -105,7 +105,7 @@ function QuerySection({
|
||||
{
|
||||
label: (
|
||||
<Tooltip title="Query Builder">
|
||||
<Button className="nav-btns" data-testid="query-builder-tab">
|
||||
<Button className="nav-btns">
|
||||
<Atom size={14} />
|
||||
<Typography.Text>Query Builder</Typography.Text>
|
||||
</Button>
|
||||
@@ -122,11 +122,7 @@ function QuerySection({
|
||||
: 'ClickHouse'
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className="nav-btns"
|
||||
disabled={isAnomalyDetection}
|
||||
data-testid="clickhouse-tab"
|
||||
>
|
||||
<Button className="nav-btns" disabled={isAnomalyDetection}>
|
||||
<Terminal size={14} />
|
||||
<Typography.Text>ClickHouse Query</Typography.Text>
|
||||
</Button>
|
||||
@@ -166,11 +162,7 @@ function QuerySection({
|
||||
: 'ClickHouse'
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className="nav-btns"
|
||||
disabled={isAnomalyDetection}
|
||||
data-testid="clickhouse-tab"
|
||||
>
|
||||
<Button className="nav-btns" disabled={isAnomalyDetection}>
|
||||
<Terminal size={14} />
|
||||
<Typography.Text>ClickHouse Query</Typography.Text>
|
||||
</Button>
|
||||
@@ -188,11 +180,7 @@ function QuerySection({
|
||||
: 'PromQL'
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className="nav-btns"
|
||||
disabled={isAnomalyDetection}
|
||||
data-testid="promql-tab"
|
||||
>
|
||||
<Button className="nav-btns" disabled={isAnomalyDetection}>
|
||||
<PromQLIcon
|
||||
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
|
||||
/>
|
||||
|
||||
@@ -80,7 +80,6 @@ function RuleOptions({
|
||||
defaultValue={defaultCompareOp}
|
||||
value={alertDef.condition?.op}
|
||||
style={{ minWidth: '120px' }}
|
||||
data-testid="alert-threshold-op-select"
|
||||
onChange={(value: string | unknown): void => {
|
||||
const newOp = (value as string) || '';
|
||||
|
||||
@@ -117,7 +116,6 @@ function RuleOptions({
|
||||
defaultValue={defaultMatchType}
|
||||
style={{ minWidth: '130px' }}
|
||||
value={alertDef.condition?.matchType}
|
||||
data-testid="alert-threshold-match-type-select-v1"
|
||||
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
|
||||
>
|
||||
<Select.Option value="1">{t('option_atleastonce')}</Select.Option>
|
||||
@@ -179,7 +177,6 @@ function RuleOptions({
|
||||
style={{ minWidth: '120px' }}
|
||||
value={alertDef.evalWindow}
|
||||
onChange={onChangeEvalWindow}
|
||||
data-testid="alert-eval-window-select"
|
||||
>
|
||||
<Select.Option value="5m0s">{t('option_5min')}</Select.Option>
|
||||
<Select.Option value="10m0s">{t('option_10min')}</Select.Option>
|
||||
@@ -197,7 +194,6 @@ function RuleOptions({
|
||||
style={{ minWidth: '120px' }}
|
||||
value={alertDef.evalWindow}
|
||||
onChange={onChangeEvalWindow}
|
||||
data-testid="alert-eval-window-select"
|
||||
>
|
||||
<Select.Option value="5m0s">{t('option_5min')}</Select.Option>
|
||||
<Select.Option value="10m0s">{t('option_10min')}</Select.Option>
|
||||
@@ -399,7 +395,6 @@ function RuleOptions({
|
||||
value={alertDef?.condition?.target}
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
data-testid="alert-threshold-target-input"
|
||||
onWheel={(e): void => e.currentTarget.blur()}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -844,6 +844,8 @@ function FormAlertRules({
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
|
||||
<div
|
||||
id="top"
|
||||
className={`form-alert-rules-container ${
|
||||
@@ -966,7 +968,6 @@ function FormAlertRules({
|
||||
!isChannelConfigurationValid ||
|
||||
queryStatus === 'error'
|
||||
}
|
||||
data-testid="alert-save-button"
|
||||
>
|
||||
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
||||
</ActionButton>
|
||||
@@ -980,7 +981,6 @@ function FormAlertRules({
|
||||
}
|
||||
type="default"
|
||||
onClick={onTestRuleHandler}
|
||||
data-testid="alert-test-button"
|
||||
>
|
||||
{' '}
|
||||
{t('button_testrule')}
|
||||
@@ -989,7 +989,6 @@ function FormAlertRules({
|
||||
disabled={loading || false}
|
||||
type="default"
|
||||
onClick={onCancelHandler}
|
||||
data-testid="alert-cancel-button"
|
||||
>
|
||||
{isNewRule && t('button_cancelchanges')}
|
||||
{ruleId && !isEmpty(ruleId) && t('button_discard')}
|
||||
@@ -999,7 +998,6 @@ function FormAlertRules({
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
testId="alert-save-confirm-dialog"
|
||||
open={isConfirmSaveOpen}
|
||||
onOpenChange={setIsConfirmSaveOpen}
|
||||
title={t('confirm_save_title')}
|
||||
|
||||
@@ -174,7 +174,6 @@ function LabelSelect({
|
||||
|
||||
<div style={{ display: 'flex', width: '100%' }}>
|
||||
<Input
|
||||
data-testid="alert-labels-input-v1"
|
||||
placeholder={renderPlaceholder()}
|
||||
onChange={handleLabelChange}
|
||||
onKeyUp={(e): void => {
|
||||
|
||||
@@ -17,11 +17,7 @@ import {
|
||||
QuickFilterChangeEventData,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraFilterCustomizedEvent,
|
||||
logInfraMonitoringListViewedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import K8sBaseDetails, {
|
||||
K8sDetailsFilters,
|
||||
@@ -57,6 +53,10 @@ import styles from './InfraMonitoringHosts.module.scss';
|
||||
import { ArrowUpToLine, Filter } from '@signozhq/icons';
|
||||
import { NANO_SECOND_MULTIPLIER, useGlobalTimeStore } from 'store/globalTime';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import {
|
||||
logInfraFilterCustomizedEvent,
|
||||
logInfraMonitoringListViewedEvent,
|
||||
} from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
function Hosts(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
|
||||
import { logInfraFilterCustomizedEvent } from 'constants/events';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import {
|
||||
StatusFilterValue,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
import styles from './StatusFilter.module.scss';
|
||||
import { logInfraFilterCustomizedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
const statusOptions: Array<{
|
||||
label: string;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
import {
|
||||
@@ -10,6 +9,7 @@ import { K8sDetailsMetadataConfig } from 'container/InfraMonitoringK8sV2/Base/K8
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { TextNoData } from 'container/InfraMonitoringK8sV2/components';
|
||||
import { getStrokeColorForPercent } from 'container/InfraMonitoringK8sV2/components/EntityProgressBar.utils';
|
||||
import { SelectedItemParams } from 'container/InfraMonitoringK8sV2/hooks';
|
||||
import {
|
||||
getHostQueryPayload,
|
||||
@@ -18,26 +18,6 @@ import {
|
||||
|
||||
import infraHostsStyles from './InfraMonitoringHosts.module.scss';
|
||||
|
||||
export function getProgressColor(percent: number): string {
|
||||
if (percent >= 90) {
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
if (percent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
|
||||
export function getMemoryProgressColor(percent: number): string {
|
||||
if (percent >= 90) {
|
||||
return Color.BG_CHERRY_500;
|
||||
}
|
||||
if (percent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
|
||||
export type HostDetailMetadataConfigType =
|
||||
K8sDetailsMetadataConfig<InframonitoringtypesHostRecordDTO>;
|
||||
export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
|
||||
@@ -79,7 +59,7 @@ export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
|
||||
render: (value): React.ReactNode => (
|
||||
<Progress
|
||||
percent={Number(Number(value).toFixed(1))}
|
||||
strokeColor={getProgressColor(Number(value))}
|
||||
strokeColor={getStrokeColorForPercent('cpu', Number(value))}
|
||||
showInfo
|
||||
/>
|
||||
),
|
||||
@@ -90,7 +70,7 @@ export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
|
||||
render: (value): React.ReactNode => (
|
||||
<Progress
|
||||
percent={Number(Number(value).toFixed(1))}
|
||||
strokeColor={getMemoryProgressColor(Number(value))}
|
||||
strokeColor={getStrokeColorForPercent('memory', Number(value))}
|
||||
showInfo
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -9,6 +9,7 @@ import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
|
||||
import { getGroupByEl } from 'container/InfraMonitoringK8sV2/Base/utils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
EntityProgressThresholds,
|
||||
ExpandButtonWrapper,
|
||||
GroupedStatusCounts,
|
||||
ValidateColumnValueWrapper,
|
||||
@@ -98,7 +99,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'hostName',
|
||||
id: INFRA_MONITORING_ATTR_KEYS.HOST_NAME,
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Hostname"
|
||||
@@ -108,7 +109,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
),
|
||||
accessorFn: (row): string => row.hostName ?? '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableSort: true,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
@@ -168,7 +169,10 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
{
|
||||
id: 'cpu',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#cpu-usage">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/host-monitoring#cpu-usage"
|
||||
tooltip={<EntityProgressThresholds type="cpu" />}
|
||||
>
|
||||
CPU Usage
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -195,7 +199,9 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
id: 'memory',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
tooltip="Excluding cache memory."
|
||||
tooltip={
|
||||
<EntityProgressThresholds type="memory" note="Excluding cache memory." />
|
||||
}
|
||||
docPath="/infrastructure-monitoring/host-monitoring#memory-usage"
|
||||
>
|
||||
Memory Usage (WSS)
|
||||
@@ -221,9 +227,12 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'diskUsage',
|
||||
id: 'disk_usage',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#disk-usage">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/host-monitoring#disk-usage"
|
||||
tooltip={<EntityProgressThresholds type="disk" />}
|
||||
>
|
||||
Disk Usage
|
||||
</ColumnHeader>
|
||||
),
|
||||
|
||||
@@ -3,13 +3,14 @@ import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
|
||||
import styles from './ColumnHeader.module.scss';
|
||||
import cx from 'classnames';
|
||||
import { MouseEventHandler } from 'react';
|
||||
|
||||
const DOCS_BASE_URL = `${process.env.DOCS_BASE_URL}/docs`;
|
||||
|
||||
interface ColumnHeaderProps {
|
||||
children?: React.ReactNode;
|
||||
docPath?: string;
|
||||
tooltip?: string;
|
||||
tooltip?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -19,6 +20,9 @@ function ColumnHeader({
|
||||
tooltip,
|
||||
className,
|
||||
}: ColumnHeaderProps): JSX.Element {
|
||||
const stopPropagationHandler: MouseEventHandler = (e): void =>
|
||||
e.stopPropagation();
|
||||
|
||||
const renderContent = (): React.ReactNode => {
|
||||
if (children) {
|
||||
return children;
|
||||
@@ -30,21 +34,25 @@ function ColumnHeader({
|
||||
const renderInfoIcon = (): React.ReactNode => {
|
||||
if (docPath) {
|
||||
const tooltipTitle = tooltip || 'Not sure what this means?';
|
||||
const isJustStringTitle = typeof tooltipTitle === 'string';
|
||||
|
||||
return (
|
||||
<TooltipSimple
|
||||
arrow
|
||||
title={
|
||||
<>
|
||||
<div onClick={stopPropagationHandler}>
|
||||
{tooltipTitle}{' '}
|
||||
<a
|
||||
href={`${DOCS_BASE_URL}${docPath}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
onClick={stopPropagationHandler}
|
||||
>
|
||||
Learn more.
|
||||
{isJustStringTitle
|
||||
? 'Learn more.'
|
||||
: 'Check the documentation to learn more.'}
|
||||
</a>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={styles.infoIcon}>
|
||||
@@ -56,7 +64,9 @@ function ColumnHeader({
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<TooltipSimple title={tooltip}>
|
||||
<TooltipSimple
|
||||
title={<div onClick={stopPropagationHandler}>{tooltip}</div>}
|
||||
>
|
||||
<div className={styles.infoIcon}>
|
||||
<Info size="md" />
|
||||
</div>
|
||||
|
||||
@@ -12,11 +12,7 @@ import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraDrawerTabViewedEvent,
|
||||
logInfraExplorerNavigatedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
@@ -50,6 +46,8 @@ import { K8sBaseDetailsContentProps } from './types';
|
||||
import { getDrawerDurationMs } from './useDrawerLifecycleStore';
|
||||
|
||||
import styles from '../EntityDetailsUtils/entityDetails.module.scss';
|
||||
import { logInfraDrawerTabViewedEvent } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/events';
|
||||
import { logInfraExplorerNavigatedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function K8sBaseDetailsContent<T>({
|
||||
|
||||
@@ -9,11 +9,7 @@ import TanStackTable, {
|
||||
useHiddenColumnIds,
|
||||
useTableParams,
|
||||
} from 'components/TanStackTableView';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraColumnSortedEvent,
|
||||
logInfraTimeRangeCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
@@ -48,6 +44,10 @@ import { K8sInstrumentationChecksCallout } from './components/K8sInstrumentation
|
||||
|
||||
import styles from './K8sBaseList.module.scss';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
logInfraColumnSortedEvent,
|
||||
logInfraTimeRangeCustomizedEvent,
|
||||
} from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
export type K8sBaseListEmptyStateContext = {
|
||||
isError: boolean;
|
||||
@@ -128,6 +128,8 @@ export function K8sBaseList<
|
||||
|
||||
const { containerRef, calculatedPageSize } = useCalculatedPageSize({
|
||||
rowHeight: 42,
|
||||
headerHeight: 58,
|
||||
paginationHeight: 52,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -436,16 +438,17 @@ export function K8sBaseList<
|
||||
isFetching={isFetching}
|
||||
cancelQuery={cancelQuery}
|
||||
/>
|
||||
|
||||
<K8sInstrumentationChecksCallout entity={entity} />
|
||||
|
||||
<K8sTableToolbar
|
||||
entity={entity}
|
||||
eventCategory={eventCategory}
|
||||
leftFilters={leftFilters}
|
||||
onOpenOptionsDrawer={handleOpenOptionsDrawer}
|
||||
/>
|
||||
|
||||
<div ref={containerRef} className={styles.tableContainer}>
|
||||
<K8sInstrumentationChecksCallout entity={entity} />
|
||||
|
||||
<K8sTableToolbar
|
||||
entity={entity}
|
||||
eventCategory={eventCategory}
|
||||
leftFilters={leftFilters}
|
||||
onOpenOptionsDrawer={handleOpenOptionsDrawer}
|
||||
/>
|
||||
|
||||
{isError && (
|
||||
<Typography>
|
||||
{data?.error?.toString() || 'Something went wrong'}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
--tanstack-table-resize-handle-hover-bg: var(--l1-border);
|
||||
--tanstack-table-row-height: 36px;
|
||||
|
||||
--tanstack-cell-padding-left-override: 15px;
|
||||
--tanstack-cell-padding-left-override: 26px;
|
||||
--tanstack-cell-padding-right-override: 15px;
|
||||
|
||||
& [data-hide-expanded='true'] {
|
||||
|
||||
@@ -10,19 +10,19 @@ import TanStackTable, {
|
||||
TableColumnDef,
|
||||
TanStackTableStateProvider,
|
||||
} from 'components/TanStackTableView';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { CornerDownRight } from '@signozhq/icons';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
|
||||
import { logInfraColumnSortedEvent } from 'constants/events';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
import {
|
||||
SelectedItemParams,
|
||||
useInfraMonitoringGroupBy,
|
||||
@@ -36,6 +36,9 @@ import { useInfraMonitoringFontSize } from './useInfraMonitoringTablePreferences
|
||||
|
||||
import styles from './K8sExpandedRow.module.scss';
|
||||
import { buildExpressionFromGroupMeta } from './utils';
|
||||
import { logInfraColumnSortedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
import { getUnstableCurrentSearchParams } from 'container/TopNav/DateTimeSelectionV2/utils/getUnstableCurrentSearchParams';
|
||||
import { QueryParams } from 'constants/query';
|
||||
|
||||
const EXPANDED_ROW_LIMIT = 10;
|
||||
|
||||
@@ -92,7 +95,6 @@ export function K8sExpandedRow<
|
||||
const [, setSelectedItemParams] = useInfraMonitoringSelectedItemParams();
|
||||
const [, setMainOrderBy] = useInfraMonitoringOrderBy();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const urlQuery = useUrlQuery();
|
||||
const location = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -258,13 +260,26 @@ export function K8sExpandedRow<
|
||||
},
|
||||
};
|
||||
|
||||
const newUrlQuery = new URLSearchParams(urlQuery.toString());
|
||||
newUrlQuery.set(
|
||||
const searchParams = getUnstableCurrentSearchParams();
|
||||
|
||||
searchParams.set(
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(updatedQuery)),
|
||||
);
|
||||
|
||||
safeNavigate(`${location.pathname}?${newUrlQuery.toString()}`);
|
||||
searchParams.delete(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||
searchParams.delete(INFRA_MONITORING_K8S_PARAMS_KEYS.EXPANDED);
|
||||
searchParams.delete(orderByParamKey);
|
||||
searchParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE, '1');
|
||||
|
||||
if (orderBy) {
|
||||
searchParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
|
||||
JSON.stringify(orderBy),
|
||||
);
|
||||
}
|
||||
|
||||
safeNavigate(`${location.pathname}?${searchParams.toString()}`);
|
||||
};
|
||||
|
||||
const total = data?.total ?? 0;
|
||||
@@ -276,6 +291,7 @@ export function K8sExpandedRow<
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
className={styles.viewAllButton}
|
||||
data-testid="expanded-row-view-all"
|
||||
onClick={handleViewAllClick}
|
||||
prefix={<CornerDownRight size={14} />}
|
||||
>
|
||||
|
||||
@@ -2,10 +2,7 @@ import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraFilterCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
@@ -24,6 +21,7 @@ import {
|
||||
import { useInfraMonitoringPageListing } from '../hooks';
|
||||
|
||||
import styles from './K8sHeader.module.scss';
|
||||
import { logInfraFilterCustomizedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
interface K8sHeaderProps {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
|
||||
@@ -4,19 +4,34 @@ import { Select } from 'antd';
|
||||
import { Download, SlidersVertical } from '@signozhq/icons';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraGroupByCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
INFRA_MONITORING_ATTR_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
import {
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringPageListing,
|
||||
} from '../hooks';
|
||||
import { useInfraMonitoringGroupByData } from './useInfraMonitoringGroupByData';
|
||||
|
||||
import styles from './K8sTableToolbar.module.scss';
|
||||
import { logInfraGroupByCustomizedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
const NAME_COLUMN_KEYS: Set<string> = new Set([
|
||||
INFRA_MONITORING_ATTR_KEYS.HOST_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME,
|
||||
]);
|
||||
|
||||
interface K8sTableToolbarProps {
|
||||
entity: InfraMonitoringEntity;
|
||||
@@ -37,11 +52,17 @@ function K8sTableToolbar({
|
||||
useInfraMonitoringGroupByData(entity);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
const [, setCurrentPage] = useInfraMonitoringPageListing();
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: string[]) => {
|
||||
void setCurrentPage(1);
|
||||
|
||||
if (orderBy && NAME_COLUMN_KEYS.has(orderBy.columnName)) {
|
||||
void setOrderBy(null);
|
||||
}
|
||||
|
||||
void setGroupBy(value);
|
||||
|
||||
void logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
@@ -52,15 +73,16 @@ function K8sTableToolbar({
|
||||
|
||||
logInfraGroupByCustomizedEvent(entity, value);
|
||||
},
|
||||
[entity, eventCategory, setCurrentPage, setGroupBy],
|
||||
[entity, eventCategory, orderBy, setCurrentPage, setOrderBy, setGroupBy],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.toolbar}>
|
||||
<div className={styles.groupByContainer}>
|
||||
<div className={styles.groupByContainer} data-testid="k8s-table-group-by">
|
||||
<div className={styles.groupByLabel}>Group by</div>
|
||||
<Select
|
||||
className={styles.groupBySelect}
|
||||
data-testid="k8s-table-group-by-select"
|
||||
loading={isLoadingGroupByFilters}
|
||||
mode="multiple"
|
||||
value={groupBy}
|
||||
|
||||
@@ -1370,4 +1370,127 @@ describe('K8sBaseList', () => {
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupBy change clears orderBy', () => {
|
||||
const onUrlUpdateMock = jest.fn<void, [UrlUpdateEvent]>();
|
||||
const fetchListDataMock = jest.fn<
|
||||
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
|
||||
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
|
||||
>();
|
||||
|
||||
beforeEach(() => {
|
||||
onUrlUpdateMock.mockClear();
|
||||
fetchListDataMock.mockClear();
|
||||
fetchListDataMock.mockResolvedValue({
|
||||
data: [{ id: 'item-1' }],
|
||||
total: 1,
|
||||
error: null,
|
||||
});
|
||||
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
|
||||
res(ctx.json({ status: 'success', data: { ready: true } })),
|
||||
),
|
||||
rest.get('http://localhost/api/v1/fields/keys', (_, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
keys: {
|
||||
resource: [{ name: 'k8s.namespace.name' }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear orderBy for name columns when groupBy is changed', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderComponent<TestItem>({
|
||||
onUrlUpdate: onUrlUpdateMock,
|
||||
entity: InfraMonitoringEntity.PODS,
|
||||
eventCategory: InfraMonitoringEvents.Pod,
|
||||
fetchListData: fetchListDataMock,
|
||||
queryParams: {
|
||||
// k8s.pod.name is a name column - should be cleared
|
||||
orderBy: JSON.stringify({ columnName: 'k8s.pod.name', order: 'desc' }),
|
||||
},
|
||||
tableColumns: createTestColumns(),
|
||||
getRowKey: (row): string => row.id,
|
||||
getItemKey: (row): string => row.id,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('k8s-table-group-by')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Open group by dropdown using testId
|
||||
const groupByContainer = screen.getByTestId('k8s-table-group-by-select');
|
||||
const groupBySelect = groupByContainer.querySelector(
|
||||
'.ant-select-selector',
|
||||
) as Element;
|
||||
await user.click(groupBySelect);
|
||||
|
||||
// Wait for options to load and click on the namespace option
|
||||
const namespaceOption = await screen.findByTitle('k8s.namespace.name');
|
||||
await user.click(namespaceOption);
|
||||
|
||||
// Verify orderBy was cleared (set to null) for name column
|
||||
await waitFor(() => {
|
||||
const orderByCalls = onUrlUpdateMock.mock.calls
|
||||
.map((call) => call[0].searchParams.get('orderBy'))
|
||||
.filter((v) => v !== undefined);
|
||||
|
||||
const hasOrderByCleared = orderByCalls.some((v) => v === null);
|
||||
expect(hasOrderByCleared).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep orderBy for non-name columns when groupBy is changed', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderComponent<TestItem>({
|
||||
onUrlUpdate: onUrlUpdateMock,
|
||||
entity: InfraMonitoringEntity.PODS,
|
||||
eventCategory: InfraMonitoringEvents.Pod,
|
||||
fetchListData: fetchListDataMock,
|
||||
queryParams: {
|
||||
// cpu is NOT a name column - should be kept
|
||||
orderBy: JSON.stringify({ columnName: 'cpu', order: 'desc' }),
|
||||
},
|
||||
tableColumns: createTestColumns(),
|
||||
getRowKey: (row): string => row.id,
|
||||
getItemKey: (row): string => row.id,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('k8s-table-group-by')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Open group by dropdown using testId
|
||||
const groupByContainer = screen.getByTestId('k8s-table-group-by-select');
|
||||
const groupBySelect = groupByContainer.querySelector(
|
||||
'.ant-select-selector',
|
||||
) as Element;
|
||||
await user.click(groupBySelect);
|
||||
|
||||
// Wait for options to load and click on the namespace option
|
||||
const namespaceOption = await screen.findByTitle('k8s.namespace.name');
|
||||
await user.click(namespaceOption);
|
||||
|
||||
// Verify orderBy was NOT cleared for non-name column
|
||||
await waitFor(() => {
|
||||
const orderByCalls = onUrlUpdateMock.mock.calls
|
||||
.map((call) => call[0].searchParams.get('orderBy'))
|
||||
.filter((v) => v !== undefined);
|
||||
|
||||
// orderBy should never be set to null
|
||||
const hasOrderByCleared = orderByCalls.some((v) => v === null);
|
||||
expect(hasOrderByCleared).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { TableColumnDef, useColumnStore } from 'components/TanStackTableView';
|
||||
import { logInfraColumnCustomizedEvent } from 'constants/events';
|
||||
|
||||
import { InfraMonitoringEntity } from '../../constants';
|
||||
import { useInfraMonitoringTablePreferencesStore } from '../useInfraMonitoringTablePreferencesStore';
|
||||
import { useLogEventForColumnCustomized } from '../useLogEventForColumnCustomized';
|
||||
import { logInfraColumnCustomizedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
jest.mock('constants/events', () => ({
|
||||
jest.mock('container/InfraMonitoringK8sV2/Base/events', () => ({
|
||||
logInfraColumnCustomizedEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Compass } from '@signozhq/icons';
|
||||
import { TextNoData } from '../../../components/TextNoData';
|
||||
import { logInfraExplorerNavigatedEvent } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
} from '../../../constants';
|
||||
import { getDrawerDurationMs } from '../../useDrawerLifecycleStore';
|
||||
import styles from './EntityCountsSection.module.scss';
|
||||
import { logInfraExplorerNavigatedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
export interface EntityCountConfig<T> {
|
||||
label: string;
|
||||
|
||||
113
frontend/src/container/InfraMonitoringK8sV2/Base/events.ts
Normal file
113
frontend/src/container/InfraMonitoringK8sV2/Base/events.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getNavigationReferrer } from 'lib/navigation';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { isCustomTimeRange } from 'store/globalTime';
|
||||
|
||||
export function logInfraFilterCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
source: 'quick_filter' | 'search' | 'host_status_toggle',
|
||||
expression: string,
|
||||
extraKeys?: string[],
|
||||
): void {
|
||||
const expressionKeys = extractQueryPairs(expression?.trim() || '').map(
|
||||
(pair) => pair.key,
|
||||
);
|
||||
|
||||
if (extraKeys) {
|
||||
extraKeys.forEach((key) => expressionKeys.push(key));
|
||||
}
|
||||
|
||||
if (expressionKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void logEvent('infra_filter_customized', {
|
||||
entity_type: entityType,
|
||||
source,
|
||||
expression_keys: [...new Set(expressionKeys)],
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraMonitoringListViewedEvent(
|
||||
entity: InfraMonitoringEntity,
|
||||
): void {
|
||||
const referrer = getNavigationReferrer();
|
||||
|
||||
void logEvent('infra_list_viewed', {
|
||||
entity,
|
||||
referrer,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraTimeRangeCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
rangeLabel: string,
|
||||
): void {
|
||||
void logEvent('infra_time_range_customized', {
|
||||
entity_type: entityType,
|
||||
range_label: isCustomTimeRange(rangeLabel) ? 'custom' : rangeLabel,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraColumnCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
columnsList: string[],
|
||||
fontSize: string,
|
||||
maxLinesPerRow: number,
|
||||
source: 'list' | 'expanded',
|
||||
): void {
|
||||
void logEvent('infra_column_customized', {
|
||||
entity_type: entityType,
|
||||
columns_list: columnsList,
|
||||
font_size: fontSize,
|
||||
max_lines_per_row: maxLinesPerRow,
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraColumnSortedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
columnKey: string,
|
||||
direction: 'asc' | 'desc',
|
||||
source: 'list' | 'expanded',
|
||||
): void {
|
||||
void logEvent('infra_column_sorted', {
|
||||
entity_type: entityType,
|
||||
column_key: columnKey,
|
||||
direction,
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraGroupByCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
groupByKeysList: string[],
|
||||
): void {
|
||||
void logEvent('infra_group_by_customized', {
|
||||
entity_type: entityType,
|
||||
group_by_keys_list: groupByKeysList,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraExplorerNavigatedEvent(params: {
|
||||
entityType: InfraMonitoringEntity;
|
||||
destination:
|
||||
| 'metrics_explorer'
|
||||
| 'logs_explorer'
|
||||
| 'traces_explorer'
|
||||
| 'k8s_list';
|
||||
source: 'chart_compass_icon' | 'tab_cta_button' | 'stats_card';
|
||||
tab: string;
|
||||
sourceKey: string | null;
|
||||
drawerDurationMsAtNavigation: number | null;
|
||||
}): void {
|
||||
void logEvent('infra_explorer_navigated', {
|
||||
entity_type: params.entityType,
|
||||
destination: params.destination,
|
||||
source: params.source,
|
||||
tab: params.tab,
|
||||
source_key: params.sourceKey,
|
||||
drawer_duration_ms_at_navigation: params.drawerDurationMsAtNavigation,
|
||||
});
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
useColumnOrder,
|
||||
useHiddenColumnIds,
|
||||
} from 'components/TanStackTableView';
|
||||
import { logInfraColumnCustomizedEvent } from 'constants/events';
|
||||
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
useInfraMonitoringLineClamp,
|
||||
} from './useInfraMonitoringTablePreferencesStore';
|
||||
import { sortByColumnOrder } from './utils';
|
||||
import { logInfraColumnCustomizedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
interface UseEmitColumnCustomizedParams<TData> {
|
||||
entity: InfraMonitoringEntity;
|
||||
|
||||
@@ -60,7 +60,7 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'clusterName',
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME,
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Cluster Name"
|
||||
@@ -70,7 +70,7 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
|
||||
),
|
||||
accessorFn: (row): string => row.clusterName || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableSort: true,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
|
||||
@@ -10,6 +10,7 @@ import { SelectedItemParams } from '../hooks';
|
||||
import { formatBytes, getPodStatusItems } from '../commonUtils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
EntityProgressThresholds,
|
||||
GroupedStatusCounts,
|
||||
TextNoData,
|
||||
ValidateColumnValueWrapper,
|
||||
@@ -69,7 +70,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'daemonsetName',
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME,
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="DaemonSet Name"
|
||||
@@ -80,7 +81,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableSort: true,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
@@ -174,7 +175,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-req-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-request" />}
|
||||
>
|
||||
CPU Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -192,7 +196,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
<EntityProgressBar value={cpuRequest} type="cpu-request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -200,7 +204,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-limit-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-limit" />}
|
||||
>
|
||||
CPU Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -217,7 +224,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
<EntityProgressBar value={cpuLimit} type="cpu-limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -251,7 +258,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-req-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-request" />}
|
||||
>
|
||||
Memory Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -269,7 +279,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
<EntityProgressBar value={memoryRequest} type="memory-request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -277,7 +287,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-limit-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-limit" />}
|
||||
>
|
||||
Memory Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -294,7 +307,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
<EntityProgressBar value={memoryLimit} type="memory-limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import { SelectedItemParams } from '../hooks';
|
||||
import { formatBytes, getPodStatusItems } from '../commonUtils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
EntityProgressThresholds,
|
||||
GroupedStatusCounts,
|
||||
TextNoData,
|
||||
ValidateColumnValueWrapper,
|
||||
@@ -70,7 +71,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'deploymentName',
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Deployment Name"
|
||||
@@ -81,7 +82,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableSort: true,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
@@ -162,7 +163,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-req-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-request" />}
|
||||
>
|
||||
CPU Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -180,7 +184,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
<EntityProgressBar value={cpuRequest} type="cpu-request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -188,7 +192,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-limit-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-limit" />}
|
||||
>
|
||||
CPU Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -205,7 +212,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
<EntityProgressBar value={cpuLimit} type="cpu-limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -238,7 +245,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-req-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/deployments#mem-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-request" />}
|
||||
>
|
||||
Memory Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -256,7 +266,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
<EntityProgressBar value={memoryRequest} type="memory-request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -264,7 +274,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-limit-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/deployments#mem-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-limit" />}
|
||||
>
|
||||
Memory Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -281,7 +294,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
<EntityProgressBar value={memoryLimit} type="memory-limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -3,10 +3,7 @@ import { Undo } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraDrawerTimeRangeCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
@@ -17,6 +14,7 @@ import {
|
||||
import { useEntityDetailsTime } from './useEntityDetailsTime';
|
||||
|
||||
import styles from './EntityDateTimeSelector.module.scss';
|
||||
import { logInfraDrawerTimeRangeCustomizedEvent } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/events';
|
||||
|
||||
interface EntityDateTimeSelectorProps {
|
||||
eventEntity: string;
|
||||
|
||||
@@ -16,10 +16,7 @@ import {
|
||||
combineInitialAndUserExpression,
|
||||
getUserExpressionFromCombined,
|
||||
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraDrawerFilterCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import Controls from 'container/Controls';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import LoadingContainer from 'container/InfraMonitoringK8sV2/LoadingContainer';
|
||||
@@ -41,6 +38,7 @@ import { getEntityEventsQueryPayload, isEventsKeyNotFoundError } from './utils';
|
||||
|
||||
import styles from './EntityEvents.module.scss';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { logInfraDrawerFilterCustomizedEvent } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/events';
|
||||
|
||||
interface EventDataType {
|
||||
key: string;
|
||||
|
||||
@@ -20,10 +20,7 @@ import {
|
||||
combineInitialAndUserExpression,
|
||||
getUserExpressionFromCombined,
|
||||
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraDrawerFilterCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
@@ -53,6 +50,7 @@ import { isModifierKeyPressed } from 'utils/app';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { logInfraDrawerFilterCustomizedEvent } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/events';
|
||||
|
||||
interface Props {
|
||||
eventEntity: string;
|
||||
|
||||
@@ -2,10 +2,7 @@ import { useCallback, useMemo, useRef } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Skeleton } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraExplorerNavigatedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
@@ -35,6 +32,7 @@ import { isKeyNotFoundError } from '../utils';
|
||||
|
||||
import styles from './EntityMetrics.module.scss';
|
||||
import { MetricsTable } from './MetricsTable';
|
||||
import { logInfraExplorerNavigatedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
interface EntityMetricsProps<T> {
|
||||
entity: T;
|
||||
|
||||
@@ -16,10 +16,7 @@ import {
|
||||
getUserExpressionFromCombined,
|
||||
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraDrawerFilterCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import Controls from 'container/Controls';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
@@ -41,6 +38,7 @@ import { getEntityTracesQueryPayload } from './utils';
|
||||
|
||||
import styles from './EntityTraces.module.scss';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { logInfraDrawerFilterCustomizedEvent } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/events';
|
||||
|
||||
interface Props {
|
||||
eventEntity: string;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { isCustomTimeRange } from 'store/globalTime';
|
||||
|
||||
export function logInfraDrawerTimeRangeCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
rangeLabel: string,
|
||||
): void {
|
||||
void logEvent('infra_drawer_time_range_customized', {
|
||||
entity_type: entityType,
|
||||
range_label: isCustomTimeRange(rangeLabel) ? 'custom' : rangeLabel,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraDrawerFilterCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
tab: 'metrics' | 'logs' | 'traces' | 'events' | 'pod_metrics',
|
||||
expression: string,
|
||||
filterSource: 'search' | 'logs',
|
||||
): void {
|
||||
const expressionKeys = extractQueryPairs(expression?.trim() || '').map(
|
||||
(pair) => pair.key,
|
||||
);
|
||||
|
||||
if (expressionKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void logEvent('infra_drawer_filter_customized', {
|
||||
entity_type: entityType,
|
||||
tab,
|
||||
expression_keys: [...new Set(expressionKeys)],
|
||||
filter_source: filterSource,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraDrawerTabViewedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
tab: string,
|
||||
isDefaultTab: boolean,
|
||||
): void {
|
||||
void logEvent('infra_drawer_tab_viewed', {
|
||||
entity_type: entityType,
|
||||
tab,
|
||||
is_default_tab: isDefaultTab,
|
||||
});
|
||||
}
|
||||
@@ -51,14 +51,14 @@ import {
|
||||
} from './hooks';
|
||||
|
||||
import styles from './InfraMonitoringK8s.module.scss';
|
||||
import {
|
||||
logInfraFilterCustomizedEvent,
|
||||
logInfraMonitoringListViewedEvent,
|
||||
InfraMonitoringEvents,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { NANO_SECOND_MULTIPLIER, useGlobalTimeStore } from 'store/globalTime';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import {
|
||||
logInfraFilterCustomizedEvent,
|
||||
logInfraMonitoringListViewedEvent,
|
||||
} from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
export default function InfraMonitoringK8s(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { SelectedItemParams } from '../hooks';
|
||||
import { formatBytes, getPodStatusItems } from '../commonUtils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
EntityProgressThresholds,
|
||||
GroupedStatusCounts,
|
||||
TextNoData,
|
||||
ValidateColumnValueWrapper,
|
||||
@@ -63,7 +64,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'jobName',
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME,
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Job Name"
|
||||
@@ -74,7 +75,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableSort: true,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
@@ -158,7 +159,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-req-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-request" />}
|
||||
>
|
||||
CPU Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -176,7 +180,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
attribute="CPU Request"
|
||||
rowId={rowId}
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
<EntityProgressBar value={cpuRequest} type="cpu-request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -184,7 +188,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-limit-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-limit" />}
|
||||
>
|
||||
CPU Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -201,7 +208,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
attribute="CPU Limit"
|
||||
rowId={rowId}
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
<EntityProgressBar value={cpuLimit} type="cpu-limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -234,7 +241,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-req-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/jobs#mem-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-request" />}
|
||||
>
|
||||
Memory Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -252,7 +262,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
attribute="Memory Request"
|
||||
rowId={rowId}
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
<EntityProgressBar value={memoryRequest} type="memory-request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -260,7 +270,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-limit-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/jobs#mem-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-limit" />}
|
||||
>
|
||||
Memory Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -277,7 +290,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
attribute="Memory Limit"
|
||||
rowId={rowId}
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
<EntityProgressBar value={memoryLimit} type="memory-limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -111,7 +111,8 @@ export const namespaceWidgetInfo = [
|
||||
{
|
||||
title: 'CPU Usage (cores)',
|
||||
yAxisUnit: '',
|
||||
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#cpu-usage-cores',
|
||||
docPath:
|
||||
'/infrastructure-monitoring/kubernetes/namespaces/#cpu-usage-cores-1',
|
||||
},
|
||||
{
|
||||
title: 'Memory Usage (bytes)',
|
||||
|
||||
@@ -66,7 +66,7 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'namespaceName',
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME,
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Namespace Name"
|
||||
@@ -76,7 +76,7 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
|
||||
),
|
||||
accessorFn: (row): string => row.namespaceName || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableSort: true,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
|
||||
@@ -57,7 +57,7 @@ export const nodeWidgetInfo = [
|
||||
{
|
||||
title: 'CPU Usage (cores)',
|
||||
yAxisUnit: '',
|
||||
docPath: '/infrastructure-monitoring/kubernetes/nodes/#cpu-usage-cores',
|
||||
docPath: '/infrastructure-monitoring/kubernetes/nodes/#cpu-usage-cores-1',
|
||||
},
|
||||
{
|
||||
title: 'Memory Usage (bytes)',
|
||||
|
||||
@@ -68,7 +68,7 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'nodeName',
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME,
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Node Name"
|
||||
@@ -78,7 +78,7 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
|
||||
),
|
||||
accessorFn: (row): string => row.nodeName || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableSort: true,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
|
||||
@@ -67,7 +67,7 @@ export const podWidgetInfo = [
|
||||
{
|
||||
title: 'CPU Usage (cores)',
|
||||
yAxisUnit: '',
|
||||
docPath: '/infrastructure-monitoring/kubernetes/pods/#cpu-usage-cores',
|
||||
docPath: '/infrastructure-monitoring/kubernetes/pods/#cpu-usage-cores-1',
|
||||
},
|
||||
{
|
||||
title: 'CPU Request, Limit Utilization',
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '../commonUtils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
EntityProgressThresholds,
|
||||
GroupedStatusCounts,
|
||||
TextNoData,
|
||||
ValidateColumnValueWrapper,
|
||||
@@ -68,7 +69,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'podName',
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME,
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Pod Name"
|
||||
@@ -79,7 +80,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableSort: true,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
@@ -96,7 +97,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): string => row.podStatus,
|
||||
width: { min: 160 },
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
@@ -175,7 +176,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
),
|
||||
accessorFn: (row): number => row.podRestarts,
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
enableSort: false,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
const restarts = value as number;
|
||||
return (
|
||||
@@ -193,7 +194,10 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-req-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/pods#cpu-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-request" />}
|
||||
>
|
||||
CPU Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -210,7 +214,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
<EntityProgressBar value={cpuRequest} type="cpu-request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -218,7 +222,10 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-limit-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/pods#cpu-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-limit" />}
|
||||
>
|
||||
CPU Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -234,7 +241,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
<EntityProgressBar value={cpuLimit} type="cpu-limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -266,7 +273,10 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-req-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/pods#mem-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-request" />}
|
||||
>
|
||||
Memory Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -283,7 +293,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
<EntityProgressBar value={memoryRequest} type="memory-request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -291,7 +301,10 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-limit-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/pods#mem-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-limit" />}
|
||||
>
|
||||
Memory Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -307,7 +320,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
<EntityProgressBar value={memoryLimit} type="memory-limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import { SelectedItemParams } from '../hooks';
|
||||
import { formatBytes, getPodStatusItems } from '../commonUtils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
EntityProgressThresholds,
|
||||
GroupedStatusCounts,
|
||||
TextNoData,
|
||||
ValidateColumnValueWrapper,
|
||||
@@ -70,7 +71,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'statefulsetName',
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME,
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="StatefulSet Name"
|
||||
@@ -81,7 +82,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableSort: true,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
@@ -165,7 +166,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-req-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-request" />}
|
||||
>
|
||||
CPU Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -183,7 +187,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
<EntityProgressBar value={cpuRequest} type="cpu-request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -191,7 +195,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-limit-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-limit" />}
|
||||
>
|
||||
CPU Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -208,7 +215,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
<EntityProgressBar value={cpuLimit} type="cpu-limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -242,7 +249,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-req-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-request" />}
|
||||
>
|
||||
Memory Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -260,7 +270,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
<EntityProgressBar value={memoryRequest} type="memory-request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -268,7 +278,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-limit-usage-">
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-limit" />}
|
||||
>
|
||||
Memory Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -285,7 +298,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
<EntityProgressBar value={memoryLimit} type="memory-limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -64,7 +64,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pvcName',
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME,
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="PVC Name"
|
||||
@@ -74,7 +74,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
|
||||
),
|
||||
accessorFn: (row): string => row.persistentVolumeClaimName || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableSort: true,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
@@ -195,7 +195,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'inodesUsed',
|
||||
id: 'inodes_used',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-inodes-used">
|
||||
Inodes Used
|
||||
@@ -219,7 +219,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'inodesFree',
|
||||
id: 'inodes_free',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-inodes-free">
|
||||
Inodes Free
|
||||
|
||||
@@ -26,48 +26,6 @@ export function formatBytes(bytes: number, decimals = 2): string {
|
||||
return `${parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns stroke color for request utilization parameters according to current value
|
||||
*/
|
||||
export function getStrokeColorForRequestUtilization(value: number): string {
|
||||
const percent = Number((value * 100).toFixed(1));
|
||||
// Orange
|
||||
if (percent <= 50) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
// Green
|
||||
if (percent > 50 && percent <= 100) {
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
// Regular Red
|
||||
if (percent > 100 && percent <= 150) {
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
// Dark Red
|
||||
return Color.BG_CHERRY_600;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns stroke color for limit utilization parameters according to current value
|
||||
*/
|
||||
export function getStrokeColorForLimitUtilization(value: number): string {
|
||||
const percent = Number((value * 100).toFixed(1));
|
||||
// Green
|
||||
if (percent <= 60) {
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
// Yellow
|
||||
if (percent > 60 && percent <= 80) {
|
||||
return Color.BG_AMBER_200;
|
||||
}
|
||||
// Orange
|
||||
if (percent > 80 && percent <= 95) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
// Red
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
|
||||
export const POD_STATUS_COLORS: Record<
|
||||
InframonitoringtypesPodStatusDTO,
|
||||
BadgeColor
|
||||
|
||||
@@ -1,35 +1,11 @@
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import {
|
||||
getMemoryProgressColor,
|
||||
getProgressColor,
|
||||
} from 'container/InfraMonitoringHostsV2/constants';
|
||||
|
||||
import {
|
||||
getStrokeColorForLimitUtilization,
|
||||
getStrokeColorForRequestUtilization,
|
||||
} from '../commonUtils';
|
||||
|
||||
import styles from './EntityProgressBar.module.scss';
|
||||
|
||||
type EntityProgressBarType = 'request' | 'limit' | 'cpu' | 'memory' | 'disk';
|
||||
|
||||
function getStrokeColor(type: EntityProgressBarType, value: number): string {
|
||||
switch (type) {
|
||||
case 'limit':
|
||||
return getStrokeColorForLimitUtilization(value);
|
||||
case 'request':
|
||||
return getStrokeColorForRequestUtilization(value);
|
||||
case 'cpu':
|
||||
return getProgressColor(Number((value * 100).toFixed(1)));
|
||||
case 'memory':
|
||||
return getMemoryProgressColor(Number((value * 100).toFixed(1)));
|
||||
case 'disk':
|
||||
return getProgressColor(Number((value * 100).toFixed(1)));
|
||||
default:
|
||||
return getStrokeColorForRequestUtilization(value);
|
||||
}
|
||||
}
|
||||
import {
|
||||
EntityProgressBarType,
|
||||
getStrokeColor,
|
||||
} from './EntityProgressBar.utils';
|
||||
|
||||
export function EntityProgressBar({
|
||||
value,
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
|
||||
export type EntityProgressBarType =
|
||||
| 'cpu-request'
|
||||
| 'cpu-limit'
|
||||
| 'memory-request'
|
||||
| 'memory-limit'
|
||||
| 'cpu'
|
||||
| 'memory'
|
||||
| 'disk';
|
||||
|
||||
export interface EntityProgressThreshold {
|
||||
matches: (percent: number) => boolean;
|
||||
color: string;
|
||||
range: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const CPU_REQUEST_THRESHOLDS: EntityProgressThreshold[] = [
|
||||
{
|
||||
matches: (percent): boolean => percent <= 50,
|
||||
color: Color.BG_AMBER_500,
|
||||
range: '≤ 50%',
|
||||
label: 'Over-requested',
|
||||
description:
|
||||
'CPU usage is at most half of the request. The rest of the request stays reserved on the node.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 100,
|
||||
color: Color.BG_FOREST_500,
|
||||
range: '> 50% - 100%',
|
||||
label: 'Right-sized',
|
||||
description: 'CPU usage is close to the request and stays within it.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 150,
|
||||
color: Color.BG_SAKURA_500,
|
||||
range: '> 100% - 150%',
|
||||
label: 'Over request',
|
||||
description:
|
||||
'CPU usage is above the request. The extra CPU is not guaranteed and depends on spare node capacity.',
|
||||
},
|
||||
{
|
||||
matches: (): boolean => true,
|
||||
color: Color.BG_CHERRY_600,
|
||||
range: '> 150%',
|
||||
label: 'Request badly undersized',
|
||||
description:
|
||||
'CPU usage is more than 1.5x the request, so most of the CPU in use is not guaranteed.',
|
||||
},
|
||||
];
|
||||
|
||||
const CPU_LIMIT_THRESHOLDS: EntityProgressThreshold[] = [
|
||||
{
|
||||
matches: (percent): boolean => percent <= 60,
|
||||
color: Color.BG_FOREST_500,
|
||||
range: '≤ 60%',
|
||||
label: 'Healthy',
|
||||
description: 'CPU usage is well below the limit.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 80,
|
||||
color: Color.BG_AMBER_200,
|
||||
range: '> 60% - 80%',
|
||||
label: 'Watch',
|
||||
description: 'CPU usage is approaching the limit.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 95,
|
||||
color: Color.BG_AMBER_500,
|
||||
range: '> 80% - 95%',
|
||||
label: 'Near limit',
|
||||
description:
|
||||
'CPU usage is close to the limit. Usage above the limit is throttled.',
|
||||
},
|
||||
{
|
||||
matches: (): boolean => true,
|
||||
color: Color.BG_SAKURA_500,
|
||||
range: '> 95%',
|
||||
label: 'At limit',
|
||||
description:
|
||||
'CPU usage is at the limit, so the container is likely being throttled.',
|
||||
},
|
||||
];
|
||||
|
||||
const MEMORY_REQUEST_THRESHOLDS: EntityProgressThreshold[] = [
|
||||
{
|
||||
matches: (percent): boolean => percent <= 50,
|
||||
color: Color.BG_AMBER_500,
|
||||
range: '≤ 50%',
|
||||
label: 'Over-requested',
|
||||
description:
|
||||
'Memory usage is at most half of the request. The rest of the request stays reserved on the node.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 100,
|
||||
color: Color.BG_FOREST_500,
|
||||
range: '> 50% - 100%',
|
||||
label: 'Right-sized',
|
||||
description: 'Memory usage is close to the request and stays within it.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 150,
|
||||
color: Color.BG_SAKURA_500,
|
||||
range: '> 100% - 150%',
|
||||
label: 'Over request',
|
||||
description:
|
||||
'Memory usage is above the request. The extra memory is not guaranteed and is reclaimed first under node memory pressure.',
|
||||
},
|
||||
{
|
||||
matches: (): boolean => true,
|
||||
color: Color.BG_CHERRY_600,
|
||||
range: '> 150%',
|
||||
label: 'Request badly undersized',
|
||||
description:
|
||||
'Memory usage is more than 1.5x the request, so most of the memory in use is not guaranteed.',
|
||||
},
|
||||
];
|
||||
|
||||
const MEMORY_LIMIT_THRESHOLDS: EntityProgressThreshold[] = [
|
||||
{
|
||||
matches: (percent): boolean => percent <= 60,
|
||||
color: Color.BG_FOREST_500,
|
||||
range: '≤ 60%',
|
||||
label: 'Healthy',
|
||||
description: 'Memory usage is well below the limit.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 80,
|
||||
color: Color.BG_AMBER_200,
|
||||
range: '> 60% - 80%',
|
||||
label: 'Watch',
|
||||
description: 'Memory usage is approaching the limit.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 95,
|
||||
color: Color.BG_AMBER_500,
|
||||
range: '> 80% - 95%',
|
||||
label: 'Near limit',
|
||||
description:
|
||||
'Memory usage is close to the limit. Unlike CPU, memory is not throttled: reaching the limit ends in an OOM kill.',
|
||||
},
|
||||
{
|
||||
matches: (): boolean => true,
|
||||
color: Color.BG_SAKURA_500,
|
||||
range: '> 95%',
|
||||
label: 'At limit',
|
||||
description:
|
||||
'Memory usage is at the limit, so an OOM kill and container restart are likely.',
|
||||
},
|
||||
];
|
||||
|
||||
const CPU_THRESHOLDS: EntityProgressThreshold[] = [
|
||||
{
|
||||
matches: (percent): boolean => percent < 60,
|
||||
color: Color.BG_FOREST_500,
|
||||
range: '< 60%',
|
||||
label: 'Healthy',
|
||||
description: 'CPU usage is well below the available capacity.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent < 90,
|
||||
color: Color.BG_AMBER_500,
|
||||
range: '60% - 89.9%',
|
||||
label: 'Elevated',
|
||||
description: 'CPU usage is high relative to the available capacity.',
|
||||
},
|
||||
{
|
||||
matches: (): boolean => true,
|
||||
color: Color.BG_SAKURA_500,
|
||||
range: '≥ 90%',
|
||||
label: 'Critical',
|
||||
description: 'CPU usage is close to the available capacity.',
|
||||
},
|
||||
];
|
||||
|
||||
const MEMORY_THRESHOLDS: EntityProgressThreshold[] = [
|
||||
{
|
||||
matches: (percent): boolean => percent < 60,
|
||||
color: Color.BG_FOREST_500,
|
||||
range: '< 60%',
|
||||
label: 'Healthy',
|
||||
description: 'Memory usage is well below the available capacity.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent < 90,
|
||||
color: Color.BG_AMBER_500,
|
||||
range: '60% - 89.9%',
|
||||
label: 'Elevated',
|
||||
description: 'Memory usage is high relative to the available capacity.',
|
||||
},
|
||||
{
|
||||
matches: (): boolean => true,
|
||||
color: Color.BG_CHERRY_500,
|
||||
range: '≥ 90%',
|
||||
label: 'Critical',
|
||||
description:
|
||||
'Memory usage is close to the available capacity. Unlike CPU, memory is not throttled: running out ends in an OOM kill.',
|
||||
},
|
||||
];
|
||||
|
||||
const DISK_THRESHOLDS: EntityProgressThreshold[] = [
|
||||
{
|
||||
matches: (percent): boolean => percent < 60,
|
||||
color: Color.BG_FOREST_500,
|
||||
range: '< 60%',
|
||||
label: 'Healthy',
|
||||
description: 'Most of the volume is still free.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent < 90,
|
||||
color: Color.BG_AMBER_500,
|
||||
range: '60% - 89.9%',
|
||||
label: 'Elevated',
|
||||
description: 'Used space is high relative to the volume capacity.',
|
||||
},
|
||||
{
|
||||
matches: (): boolean => true,
|
||||
color: Color.BG_SAKURA_500,
|
||||
range: '≥ 90%',
|
||||
label: 'Critical',
|
||||
description: 'The volume is nearly full. Writes fail once no space is left.',
|
||||
},
|
||||
];
|
||||
|
||||
export const THRESHOLDS_BY_TYPE: Record<
|
||||
EntityProgressBarType,
|
||||
EntityProgressThreshold[]
|
||||
> = {
|
||||
'cpu-request': CPU_REQUEST_THRESHOLDS,
|
||||
'cpu-limit': CPU_LIMIT_THRESHOLDS,
|
||||
'memory-request': MEMORY_REQUEST_THRESHOLDS,
|
||||
'memory-limit': MEMORY_LIMIT_THRESHOLDS,
|
||||
cpu: CPU_THRESHOLDS,
|
||||
memory: MEMORY_THRESHOLDS,
|
||||
disk: DISK_THRESHOLDS,
|
||||
};
|
||||
|
||||
export function getStrokeColorForPercent(
|
||||
type: EntityProgressBarType,
|
||||
percent: number,
|
||||
): string {
|
||||
const thresholds = THRESHOLDS_BY_TYPE[type];
|
||||
const match = thresholds.find((threshold) => threshold.matches(percent));
|
||||
return (match ?? thresholds[thresholds.length - 1]).color;
|
||||
}
|
||||
|
||||
export function getStrokeColor(
|
||||
type: EntityProgressBarType,
|
||||
value: number,
|
||||
): string {
|
||||
return getStrokeColorForPercent(type, Number((value * 100).toFixed(1)));
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
max-width: 320px;
|
||||
text-align: left;
|
||||
text-wrap: wrap;
|
||||
margin-bottom: var(--spacing-1);
|
||||
}
|
||||
|
||||
.threshold {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.swatch {
|
||||
width: 3px;
|
||||
border-radius: 1px;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--ept-color);
|
||||
}
|
||||
|
||||
.thresholdBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-1);
|
||||
}
|
||||
|
||||
.thresholdHeading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.range {
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import {
|
||||
EntityProgressBarType,
|
||||
THRESHOLDS_BY_TYPE,
|
||||
} from './EntityProgressBar.utils';
|
||||
import styles from './EntityProgressThresholds.module.scss';
|
||||
|
||||
interface EntityProgressThresholdsProps {
|
||||
type: EntityProgressBarType;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export function EntityProgressThresholds({
|
||||
type,
|
||||
note,
|
||||
}: EntityProgressThresholdsProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
data-testid={`entity-progress-thresholds-${type}`}
|
||||
>
|
||||
{note && (
|
||||
<Typography.Text as="p" size="small">
|
||||
{note}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{THRESHOLDS_BY_TYPE[type].map((threshold) => (
|
||||
<div key={threshold.range} className={styles.threshold}>
|
||||
<span
|
||||
className={styles.swatch}
|
||||
style={{ '--ept-color': threshold.color } as React.CSSProperties}
|
||||
/>
|
||||
<div className={styles.thresholdBody}>
|
||||
<div className={styles.thresholdHeading}>
|
||||
<Typography.Text as="span" size="small" weight="medium">
|
||||
{threshold.label}
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
as="span"
|
||||
size="small"
|
||||
color="muted"
|
||||
className={styles.range}
|
||||
>
|
||||
{threshold.range}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text as="p" size="small" color="muted">
|
||||
{threshold.description}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { THRESHOLDS_BY_TYPE } from '../EntityProgressBar.utils';
|
||||
import { EntityProgressThresholds } from '../EntityProgressThresholds';
|
||||
|
||||
describe('EntityProgressThresholds', () => {
|
||||
it('renders every threshold band for the given type', () => {
|
||||
render(<EntityProgressThresholds type="cpu-limit" />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId('entity-progress-thresholds-cpu-limit'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
THRESHOLDS_BY_TYPE['cpu-limit'].forEach((threshold) => {
|
||||
expect(screen.getByText(threshold.label)).toBeInTheDocument();
|
||||
expect(screen.getByText(threshold.range)).toBeInTheDocument();
|
||||
expect(screen.getByText(threshold.description)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the note above the threshold bands when provided', () => {
|
||||
render(
|
||||
<EntityProgressThresholds type="memory" note="Excluding cache memory." />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Excluding cache memory.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
export { EntityProgressBar } from './EntityProgressBar';
|
||||
export { EntityProgressThresholds } from './EntityProgressThresholds';
|
||||
export { ValidateColumnValueWrapper } from './ValidateColumnValueWrapper';
|
||||
export { ExpandButtonWrapper } from './ExpandButtonWrapper';
|
||||
export {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { defaultFeatureFlags, render, screen } from 'tests/test-utils';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import {
|
||||
invalidLicense,
|
||||
setupAuthzAdmin,
|
||||
@@ -59,23 +58,6 @@ function renderEditPage(
|
||||
|
||||
describe('CreateEditRolePage - Feature Gate', () => {
|
||||
describe('create mode - feature disabled', () => {
|
||||
it('shows error when fine-grained authz flag is inactive', async () => {
|
||||
renderCreatePage({
|
||||
featureFlags: defaultFeatureFlags.map((f) =>
|
||||
f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ
|
||||
? { ...f, active: false }
|
||||
: f,
|
||||
),
|
||||
});
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('feature-gate-error-banner'),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText(/Custom roles feature is not available/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error when license is invalid', async () => {
|
||||
renderCreatePage({ activeLicense: invalidLicense });
|
||||
|
||||
@@ -113,23 +95,6 @@ describe('CreateEditRolePage - Feature Gate', () => {
|
||||
const ROLE_ID = '019c24aa-3333-0001-aaaa-111111111111';
|
||||
const ROLE_NAME = 'test-role';
|
||||
|
||||
it('shows error when fine-grained authz flag is inactive', async () => {
|
||||
renderEditPage(ROLE_ID, ROLE_NAME, {
|
||||
featureFlags: defaultFeatureFlags.map((f) =>
|
||||
f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ
|
||||
? { ...f, active: false }
|
||||
: f,
|
||||
),
|
||||
});
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('feature-gate-error-banner'),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText(/Custom roles feature is not available/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error when license is invalid', async () => {
|
||||
renderEditPage(ROLE_ID, ROLE_NAME, { activeLicense: invalidLicense });
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { defaultFeatureFlags, render, screen, waitFor } from 'tests/test-utils';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
import {
|
||||
invalidLicense,
|
||||
setupAuthzAdmin,
|
||||
@@ -33,26 +32,6 @@ describe('ViewRolePage - Feature Gate', () => {
|
||||
});
|
||||
|
||||
describe('feature disabled', () => {
|
||||
it('shows error when fine-grained authz flag is inactive', async () => {
|
||||
render(<ViewRolePage />, undefined, {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
appContextOverrides: {
|
||||
featureFlags: defaultFeatureFlags.map((f) =>
|
||||
f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ
|
||||
? { ...f, active: false }
|
||||
: f,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('feature-gate-error-banner'),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText(/Custom roles feature is not available/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error when license is invalid', async () => {
|
||||
render(<ViewRolePage />, undefined, {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
|
||||
@@ -4,13 +4,7 @@ import {
|
||||
} from 'mocks-server/__mockdata__/roles';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import {
|
||||
defaultFeatureFlags,
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
} from 'tests/test-utils';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import {
|
||||
invalidLicense,
|
||||
setupAuthzAdmin,
|
||||
@@ -191,30 +185,6 @@ describe('RolesSettings', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('hides the create button and disables row clicks when fine-grained authz flag is inactive', async () => {
|
||||
render(<RolesSettings />, undefined, {
|
||||
appContextOverrides: {
|
||||
featureFlags: defaultFeatureFlags.map((f) =>
|
||||
f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ
|
||||
? { ...f, active: false }
|
||||
: f,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /custom role/i }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const rows = document.querySelectorAll('.roles-table-row');
|
||||
rows.forEach((row) => {
|
||||
expect(row).not.toHaveClass('roles-table-row--clickable');
|
||||
expect(row.getAttribute('role')).not.toBe('button');
|
||||
});
|
||||
});
|
||||
|
||||
it('hides the create button and disables row clicks when license is not valid', async () => {
|
||||
render(<RolesSettings />, undefined, {
|
||||
appContextOverrides: { activeLicense: invalidLicense },
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
* This was introduced to fix a sync bug between Nuqs and react-router-dom
|
||||
*
|
||||
* We are using the wrong adapter for nuqs because the correct one only supports v6/v7,
|
||||
* and we are at version v5. This causes the nuqs/react-router-dom to be out of sync.
|
||||
* and we are at version v5. Nuqs writes params straight to the History API, which
|
||||
* react-router v5 never observes, so `useLocation().search` (and `useUrlQuery()`) can
|
||||
* be several nuqs updates behind the real URL.
|
||||
*
|
||||
* We can revert this commit once we migrate react-router-dom to v6, or once we migrate
|
||||
* to DateTimeSelectionV3
|
||||
* Use this whenever you need to build a navigation target on top of the current
|
||||
* params, otherwise stale values get republished and nuqs adopts them back on its
|
||||
* next flush (it snapshots `window.location.search`).
|
||||
*
|
||||
* We can revert this once we migrate react-router-dom to v6.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
export function useIsInfraMonitoringV2(): boolean {
|
||||
const { featureFlags } = useAppContext();
|
||||
return Boolean(
|
||||
featureFlags?.find(
|
||||
(flag) => flag.name === FeatureKeys.USE_INFRA_MONITORING_V2,
|
||||
)?.active,
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
|
||||
@@ -6,22 +5,12 @@ export const useRolesFeatureGate = (): {
|
||||
isRolesEnabled: boolean;
|
||||
isLoading: boolean;
|
||||
} => {
|
||||
const {
|
||||
activeLicense,
|
||||
featureFlags,
|
||||
isFetchingActiveLicense,
|
||||
isFetchingFeatureFlags,
|
||||
} = useAppContext();
|
||||
const { activeLicense, isFetchingActiveLicense } = useAppContext();
|
||||
|
||||
const isValidLicense = activeLicense?.status === LicenseStatus.VALID;
|
||||
const isFineGrainedAuthzEnabled =
|
||||
featureFlags?.find((f) => f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ)
|
||||
?.active ?? false;
|
||||
|
||||
return {
|
||||
isRolesEnabled: isValidLicense && isFineGrainedAuthzEnabled,
|
||||
isLoading:
|
||||
(isFetchingActiveLicense && !activeLicense) ||
|
||||
(isFetchingFeatureFlags && !featureFlags),
|
||||
isRolesEnabled: isValidLicense,
|
||||
isLoading: isFetchingActiveLicense && !activeLicense,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import getMinAgo from './getStartAndEndTime/getMinAgo';
|
||||
|
||||
const validCustomTimeRegex = /^(\d+)([mhdw])$/;
|
||||
const validCustomTimeRegex = /^(\d+)(months?|[mhdw])$/;
|
||||
|
||||
export const isValidShortHandDateTimeFormat = (time: string): boolean =>
|
||||
validCustomTimeRegex.test(time);
|
||||
|
||||
@@ -94,8 +94,6 @@ function AlertDetails(): JSX.Element {
|
||||
>
|
||||
<div
|
||||
className={classNames('alert-details', { 'alert-details-v2': isV2Alert })}
|
||||
data-testid="alert-details-root"
|
||||
data-schema-version={isV2Alert ? NEW_ALERT_SCHEMA_VERSION : 'v1'}
|
||||
>
|
||||
<AlertBreadcrumb
|
||||
className="alert-details__breadcrumb"
|
||||
|
||||
@@ -117,11 +117,7 @@ function AlertActionButtons({
|
||||
<div className="alert-action-buttons">
|
||||
<Tooltip title={isAlertRuleDisabled ? 'Enable alert' : 'Disable alert'}>
|
||||
{isAlertRuleDisabled !== undefined && (
|
||||
<Switch
|
||||
onChange={toggleAlertRule}
|
||||
value={!isAlertRuleDisabled}
|
||||
testId="alert-actions-toggle"
|
||||
/>
|
||||
<Switch onChange={toggleAlertRule} value={!isAlertRuleDisabled} />
|
||||
)}
|
||||
</Tooltip>
|
||||
<CopyToClipboard textToCopy={window.location.href} />
|
||||
@@ -133,7 +129,6 @@ function AlertActionButtons({
|
||||
<Tooltip title="More options">
|
||||
<Button
|
||||
type="text"
|
||||
data-testid="alert-actions-menu"
|
||||
icon={
|
||||
<Ellipsis
|
||||
size={16}
|
||||
|
||||
@@ -47,29 +47,21 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
<div className="alert-info__info-wrapper">
|
||||
<div className="top-section">
|
||||
<div className="alert-title-wrapper">
|
||||
<div data-testid="alert-header-state">
|
||||
<AlertState state={alertRuleState ?? state ?? ''} />
|
||||
</div>
|
||||
<div className="alert-title" data-testid="alert-header-title">
|
||||
<AlertState state={alertRuleState ?? state ?? ''} />
|
||||
<div className="alert-title">
|
||||
<LineClampedText text={displayName || ''} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bottom-section">
|
||||
{labels?.severity && (
|
||||
<div data-testid="alert-header-severity">
|
||||
<AlertSeverity severity={labels.severity} />
|
||||
</div>
|
||||
)}
|
||||
{labels?.severity && <AlertSeverity severity={labels.severity} />}
|
||||
|
||||
{/* // TODO(shaheer): Get actual data when we are able to get alert firing from state from API */}
|
||||
{/* <AlertStatus
|
||||
status="firing"
|
||||
timestamp={dayjs().subtract(1, 'd').valueOf()}
|
||||
/> */}
|
||||
<div data-testid="alert-header-labels">
|
||||
<AlertLabels labels={labelsWithoutSeverity} />
|
||||
</div>
|
||||
<AlertLabels labels={labelsWithoutSeverity} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -127,7 +127,7 @@ export const useRouteTabUtils = (): { routes: TabRoutes[] } => {
|
||||
{
|
||||
Component: EditRules,
|
||||
name: (
|
||||
<div className="tab-item" data-testid="alert-details-tab-overview">
|
||||
<div className="tab-item">
|
||||
<Table size={14} />
|
||||
Overview
|
||||
</div>
|
||||
@@ -138,7 +138,7 @@ export const useRouteTabUtils = (): { routes: TabRoutes[] } => {
|
||||
{
|
||||
Component: AlertHistory,
|
||||
name: (
|
||||
<div className="tab-item" data-testid="alert-details-tab-history">
|
||||
<div className="tab-item">
|
||||
<History size={14} />
|
||||
History
|
||||
<BetaTag />
|
||||
|
||||
@@ -52,12 +52,12 @@ const makeDashboard = (
|
||||
...overrides,
|
||||
}) as unknown as DashboardListItem;
|
||||
|
||||
const renderRow = (dashboard: DashboardListItem): void => {
|
||||
const renderRow = (dashboard: DashboardListItem, canEdit = true): void => {
|
||||
render(
|
||||
<DashboardRow
|
||||
dashboard={dashboard}
|
||||
index={0}
|
||||
canEdit
|
||||
canEdit={canEdit}
|
||||
showUpdatedAt={false}
|
||||
showUpdatedBy={false}
|
||||
/>,
|
||||
@@ -105,6 +105,19 @@ describe('DashboardRow', () => {
|
||||
|
||||
expect(mockSafeNavigate).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('legacy-dashboard-id')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('legacy-dashboard-retry-migration'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('withholds the retry action from a row the user cannot edit', async () => {
|
||||
renderRow(makeDashboard({ legacy: true }), false);
|
||||
|
||||
await userEvent.click(screen.getByTestId('dashboard-title-0'));
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('legacy-dashboard-retry-migration'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -252,6 +252,7 @@ function DashboardRow({
|
||||
open={isLegacyDialogOpen}
|
||||
dashboardId={id}
|
||||
dashboardName={name}
|
||||
canEdit={canEdit}
|
||||
onClose={(): void => setIsLegacyDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -18,19 +18,30 @@ jest.mock('container/Integrations/utils', () => ({
|
||||
handleContactSupport: (isCloud: boolean): void => mockContactSupport(isCloud),
|
||||
}));
|
||||
|
||||
const mockRetryMigration = jest.fn();
|
||||
let isMigrating = false;
|
||||
jest.mock('../../hooks/useRetryMigration', () => ({
|
||||
useRetryMigration: (): {
|
||||
retryMigration: jest.Mock;
|
||||
isMigrating: boolean;
|
||||
} => ({ retryMigration: mockRetryMigration, isMigrating }),
|
||||
}));
|
||||
|
||||
const DASHBOARD_ID = '0f9a1b2c-3d4e-5f6a-7b8c-9d0e1f2a3b4c';
|
||||
|
||||
describe('LegacyDashboardDialog', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
isMigrating = false;
|
||||
});
|
||||
|
||||
const setup = (open = true): void => {
|
||||
const setup = ({ open = true, canEdit = true } = {}): void => {
|
||||
render(
|
||||
<LegacyDashboardDialog
|
||||
open={open}
|
||||
dashboardId={DASHBOARD_ID}
|
||||
dashboardName="My Legacy Dashboard"
|
||||
canEdit={canEdit}
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
@@ -57,8 +68,31 @@ describe('LegacyDashboardDialog', () => {
|
||||
expect(mockContactSupport).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('retries the migration for the dashboard', async () => {
|
||||
setup();
|
||||
await userEvent.click(screen.getByTestId('legacy-dashboard-retry-migration'));
|
||||
expect(mockRetryMigration).toHaveBeenCalledWith(DASHBOARD_ID);
|
||||
});
|
||||
|
||||
it('blocks retry and close while the migration is in flight', () => {
|
||||
isMigrating = true;
|
||||
setup();
|
||||
expect(screen.getByTestId('legacy-dashboard-retry-migration')).toBeDisabled();
|
||||
expect(screen.getByTestId('legacy-dashboard-close')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('offers only the support path without edit access', () => {
|
||||
setup({ canEdit: false });
|
||||
expect(
|
||||
screen.queryByTestId('legacy-dashboard-retry-migration'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('legacy-dashboard-contact-support'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders nothing when closed', () => {
|
||||
setup(false);
|
||||
setup({ open: false });
|
||||
expect(screen.queryByTestId('legacy-dashboard-id')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ArrowUpRight, Copy } from '@signozhq/icons';
|
||||
import { ArrowUpRight, Copy, RotateCw } from '@signozhq/icons';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -9,28 +9,34 @@ import { handleContactSupport } from 'container/Integrations/utils';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { DashboardListEvents } from 'pages/DashboardsListPageV2/constants/events';
|
||||
|
||||
import { useRetryMigration } from '../../hooks/useRetryMigration';
|
||||
|
||||
import styles from './LegacyDashboardDialog.module.scss';
|
||||
|
||||
interface LegacyDashboardDialogProps {
|
||||
open: boolean;
|
||||
dashboardId: string;
|
||||
dashboardName: string;
|
||||
canEdit: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explains why a legacy (pre-v2) dashboard can't be opened in the new experience
|
||||
* and hands the user the dashboard ID to share with support. Legacy rows are
|
||||
* surfaced by the list API with `legacy: true` but have no v2 spec to render.
|
||||
* and offers to re-run the migration. Legacy rows are surfaced by the list API
|
||||
* with `legacy: true` but have no v2 spec to render. Retrying needs edit access,
|
||||
* so viewers only get the dashboard ID to share with support.
|
||||
*/
|
||||
function LegacyDashboardDialog({
|
||||
open,
|
||||
dashboardId,
|
||||
dashboardName,
|
||||
canEdit,
|
||||
onClose,
|
||||
}: LegacyDashboardDialogProps): JSX.Element {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const { isCloudUser } = useGetTenantLicense();
|
||||
const { retryMigration, isMigrating } = useRetryMigration(onClose);
|
||||
|
||||
const onCopyId = (): void => {
|
||||
copyToClipboard(dashboardId);
|
||||
@@ -49,6 +55,14 @@ function LegacyDashboardDialog({
|
||||
});
|
||||
};
|
||||
|
||||
const onRetryMigration = (): void => {
|
||||
retryMigration(dashboardId);
|
||||
void logEvent(DashboardListEvents.LegacyDialogAction, {
|
||||
action: 'retryMigration',
|
||||
dashboardId,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogWrapper
|
||||
title="This dashboard isn't available in the new experience"
|
||||
@@ -65,14 +79,15 @@ function LegacyDashboardDialog({
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="md"
|
||||
disabled={isMigrating}
|
||||
onClick={onClose}
|
||||
testId="legacy-dashboard-close"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
variant={canEdit ? 'outlined' : 'solid'}
|
||||
color={canEdit ? 'secondary' : 'primary'}
|
||||
size="md"
|
||||
suffix={<ArrowUpRight size={14} />}
|
||||
onClick={onContactSupport}
|
||||
@@ -80,6 +95,20 @@ function LegacyDashboardDialog({
|
||||
>
|
||||
Contact Support
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="md"
|
||||
prefix={<RotateCw size={14} />}
|
||||
disabled={isMigrating}
|
||||
loading={isMigrating}
|
||||
onClick={onRetryMigration}
|
||||
testId="legacy-dashboard-retry-migration"
|
||||
>
|
||||
Retry migration
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -87,8 +116,10 @@ function LegacyDashboardDialog({
|
||||
<Typography.Text className={styles.description}>
|
||||
<strong>{dashboardName || 'This dashboard'}</strong> hasn't been
|
||||
migrated to the new dashboard experience yet, so it can't be opened
|
||||
here. Share the dashboard ID below with support and we'll help you
|
||||
move it over.
|
||||
here.{' '}
|
||||
{canEdit
|
||||
? "Retrying the migration often works once we've handled the case that blocked it. If it still fails, share the dashboard ID below with support."
|
||||
: "Share the dashboard ID below with support and we'll help you move it over."}
|
||||
</Typography.Text>
|
||||
|
||||
<div className={styles.idField}>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import {
|
||||
invalidateListDashboardsForUserV2,
|
||||
useMigrateDashboardV2,
|
||||
} from 'api/generated/services/dashboard';
|
||||
|
||||
import { useRetryMigration } from '../useRetryMigration';
|
||||
|
||||
jest.mock('react-query', () => ({
|
||||
useQueryClient: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('api/generated/services/dashboard', () => ({
|
||||
useMigrateDashboardV2: jest.fn(),
|
||||
invalidateListDashboardsForUserV2: jest.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
const queryClient = { invalidateQueries: jest.fn() };
|
||||
const mockMutate = jest.fn();
|
||||
const onMigrated = jest.fn();
|
||||
|
||||
type MutationHandlers = {
|
||||
onSuccess: () => Promise<void>;
|
||||
onError: (error: unknown) => void;
|
||||
};
|
||||
|
||||
let captured: MutationHandlers;
|
||||
|
||||
// Stands in for the generated mutation hook: records the handlers the hook wires
|
||||
// up so each one can be driven directly, and reports the requested in-flight state.
|
||||
function setup(isLoading = false): {
|
||||
retryMigration: (id: string) => void;
|
||||
isMigrating: boolean;
|
||||
} {
|
||||
(useMigrateDashboardV2 as jest.Mock).mockImplementation(
|
||||
(options: { mutation: MutationHandlers }) => {
|
||||
captured = options.mutation;
|
||||
return { mutate: mockMutate, isLoading };
|
||||
},
|
||||
);
|
||||
return renderHook(() => useRetryMigration(onMigrated)).result.current;
|
||||
}
|
||||
|
||||
// A 501 from GET/POST on an un-migrated dashboard carries the render error envelope.
|
||||
const envelopeError = {
|
||||
response: {
|
||||
status: 501,
|
||||
data: {
|
||||
error: { code: 'dashboard_invalid_data', message: 'not in v6 schema' },
|
||||
},
|
||||
},
|
||||
message: 'Request failed with status code 501',
|
||||
};
|
||||
|
||||
// A gateway failure responds without an envelope, so there is no backend reason to show.
|
||||
const bodylessError = {
|
||||
response: { status: 502, data: '<html>bad gateway</html>' },
|
||||
message: 'Request failed with status code 502',
|
||||
};
|
||||
|
||||
describe('useRetryMigration', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useQueryClient as jest.Mock).mockReturnValue(queryClient);
|
||||
});
|
||||
|
||||
it('sends the dashboard id as a path parameter', () => {
|
||||
setup().retryMigration('dash-1');
|
||||
expect(mockMutate).toHaveBeenCalledWith({ pathParams: { id: 'dash-1' } });
|
||||
});
|
||||
|
||||
it('refreshes the list, confirms with a toast and reports success', async () => {
|
||||
setup();
|
||||
await captured.onSuccess();
|
||||
|
||||
expect(invalidateListDashboardsForUserV2).toHaveBeenCalledWith(queryClient);
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
'Dashboard migrated to the new experience',
|
||||
);
|
||||
expect(onMigrated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('surfaces the backend reason and does not report success on failure', () => {
|
||||
setup();
|
||||
captured.onError(envelopeError);
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('not in v6 schema');
|
||||
expect(onMigrated).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('points the user at support when the failure carries no reason', () => {
|
||||
setup();
|
||||
captured.onError(bodylessError);
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
'Could not migrate this dashboard. Please contact support.',
|
||||
);
|
||||
});
|
||||
|
||||
it('reports the in-flight state from the mutation', () => {
|
||||
expect(setup(true).isMigrating).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import {
|
||||
invalidateListDashboardsForUserV2,
|
||||
useMigrateDashboardV2,
|
||||
} from 'api/generated/services/dashboard';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
const FAILURE_MESSAGE =
|
||||
'Could not migrate this dashboard. Please contact support.';
|
||||
|
||||
export interface UseRetryMigrationResult {
|
||||
// Re-run the v1 to v2 migration for a dashboard.
|
||||
retryMigration: (id: string) => void;
|
||||
isMigrating: boolean;
|
||||
}
|
||||
|
||||
// Wraps the retry-migration mutation for a legacy (pre-v2) dashboard: refreshes
|
||||
// the personalized list so the row loses its legacy flag, and reports the
|
||||
// backend's reason as a toast when the dashboard still can't be converted.
|
||||
export function useRetryMigration(
|
||||
onMigrated?: () => void,
|
||||
): UseRetryMigrationResult {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const migrate = useMigrateDashboardV2({
|
||||
mutation: {
|
||||
onSuccess: async (): Promise<void> => {
|
||||
await invalidateListDashboardsForUserV2(queryClient);
|
||||
toast.success('Dashboard migrated to the new experience');
|
||||
onMigrated?.();
|
||||
},
|
||||
onError: (error): void => {
|
||||
toast.error(toAPIError(error, FAILURE_MESSAGE).getErrorMessage());
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const retryMigration = useCallback(
|
||||
(id: string): void => {
|
||||
migrate.mutate({ pathParams: { id } });
|
||||
},
|
||||
[migrate],
|
||||
);
|
||||
|
||||
return { retryMigration, isMigrating: migrate.isLoading };
|
||||
}
|
||||
@@ -114,6 +114,23 @@ describe('getCaretContext — stage detection', () => {
|
||||
expect(ctx.partial).toBe('');
|
||||
});
|
||||
|
||||
it('never replaces past the caret when it sits before the operator', () => {
|
||||
const ctx = getCaretContext("env = 'prod'", 4);
|
||||
expect(ctx.stage).toBe('operator');
|
||||
expect(ctx.partial).toBe('');
|
||||
expect(ctx.replaceStart).toBe(4);
|
||||
expect(ctx.replaceEnd).toBe(4);
|
||||
});
|
||||
|
||||
it('never replaces past the caret when it sits before the value', () => {
|
||||
const ctx = getCaretContext("env = 'prod'", 6);
|
||||
expect(ctx.stage).toBe('value');
|
||||
expect(ctx.operator).toBe('=');
|
||||
expect(ctx.partial).toBe('');
|
||||
expect(ctx.replaceStart).toBe(6);
|
||||
expect(ctx.replaceEnd).toBe(6);
|
||||
});
|
||||
|
||||
it('detects the stage of the term under a mid-string caret', () => {
|
||||
const q = "env = AND team = 'core'";
|
||||
// caret right after the first `env ` (index 4) is the operator stage
|
||||
@@ -142,6 +159,13 @@ describe('spliceAtCaret', () => {
|
||||
expect(next).toBe("env = 'prod'");
|
||||
});
|
||||
|
||||
it('inserts (without duplicating text) at a caret parked before a token', () => {
|
||||
const q = "env = 'prod'";
|
||||
const ctx = getCaretContext(q, 4);
|
||||
const { next } = spliceAtCaret(q, ctx, '!= ');
|
||||
expect(next).toBe("env != = 'prod'");
|
||||
});
|
||||
|
||||
it('preserves text after the caret', () => {
|
||||
const q = "env AND team = 'core'";
|
||||
const ctx = getCaretContext(q, 4); // operator gap after `env`
|
||||
|
||||
@@ -328,7 +328,7 @@ export const getCaretContext = (query: string, caret: number): CaretContext => {
|
||||
fieldKey: scan.key ? scan.key.text : '',
|
||||
operator: slot.operator,
|
||||
partial: slot.partial,
|
||||
replaceStart: term.start + slot.replaceStartRel,
|
||||
replaceStart: Math.min(term.start + slot.replaceStartRel, pos),
|
||||
replaceEnd: pos,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,47 +1,15 @@
|
||||
import { Suspense } from 'react';
|
||||
import Loadable from 'components/Loadable';
|
||||
import { TabRoutes } from 'components/RouteTab/types';
|
||||
import Spinner from 'components/Spinner';
|
||||
import ROUTES from 'constants/routes';
|
||||
import InfraMonitoringHosts from 'container/InfraMonitoringHosts';
|
||||
import InfraMonitoringK8s from 'container/InfraMonitoringK8s';
|
||||
import { useIsInfraMonitoringV2 } from 'hooks/useIsInfraMonitoringV2';
|
||||
import InfraMonitoringHostsV2 from 'container/InfraMonitoringHostsV2';
|
||||
import InfraMonitoringK8sV2 from 'container/InfraMonitoringK8sV2';
|
||||
import { Inbox } from '@signozhq/icons';
|
||||
|
||||
const InfraMonitoringHostsV2 = Loadable(
|
||||
() => import('container/InfraMonitoringHostsV2'),
|
||||
);
|
||||
|
||||
const InfraMonitoringK8sV2 = Loadable(
|
||||
() => import('container/InfraMonitoringK8sV2'),
|
||||
);
|
||||
|
||||
function HostsContainer(): JSX.Element {
|
||||
const isInfraMonitoringV2 = useIsInfraMonitoringV2();
|
||||
|
||||
if (isInfraMonitoringV2) {
|
||||
return (
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<InfraMonitoringHostsV2 />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <InfraMonitoringHosts />;
|
||||
return <InfraMonitoringHostsV2 />;
|
||||
}
|
||||
|
||||
function KubernetesContainer(): JSX.Element {
|
||||
const isInfraMonitoringV2 = useIsInfraMonitoringV2();
|
||||
|
||||
if (isInfraMonitoringV2) {
|
||||
return (
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<InfraMonitoringK8sV2 />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <InfraMonitoringK8s />;
|
||||
return <InfraMonitoringK8sV2 />;
|
||||
}
|
||||
|
||||
export const Hosts: TabRoutes = {
|
||||
|
||||
@@ -13,8 +13,6 @@ interface Tab {
|
||||
disabled?: boolean;
|
||||
icon?: string | JSX.Element;
|
||||
isBeta?: boolean;
|
||||
/** Optional `data-testid` for the tab button. */
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
interface TimelineTabsProps {
|
||||
@@ -65,7 +63,6 @@ function Tabs2({
|
||||
disabled={tab.disabled}
|
||||
icon={tab.icon}
|
||||
style={{ minWidth: buttonMinWidth }}
|
||||
data-testid={tab.testId}
|
||||
>
|
||||
{tab.label}
|
||||
|
||||
|
||||
@@ -150,13 +150,6 @@ export const defaultFeatureFlags = [
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
{
|
||||
name: FeatureKeys.USE_FINE_GRAINED_AUTHZ,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
];
|
||||
|
||||
export function getAppContextMock(
|
||||
|
||||
@@ -69,7 +69,12 @@ export const getMetricsExplorerUrl = ({
|
||||
const params = new URLSearchParams();
|
||||
params.set(
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(query)),
|
||||
// `unit` must always be present: the query builder provider rewrites (and
|
||||
// pushes a new history entry for) any compositeQuery missing a key of
|
||||
// `initialQueriesMap`, which traps the browser back button.
|
||||
// Since this is only being used by infra-monitoring, I will keep this fix one line
|
||||
// instead of going and update each chart configuration.
|
||||
encodeURIComponent(JSON.stringify({ unit: '', ...query })),
|
||||
);
|
||||
|
||||
if (relativeTime) {
|
||||
|
||||
@@ -241,9 +241,12 @@ func (server *Server) PutAlerts(ctx context.Context, postableAlerts alertmanager
|
||||
}
|
||||
|
||||
func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertmanagertypes.Config) error {
|
||||
config := alertmanagerConfig.AlertmanagerConfig()
|
||||
resolved, err := alertmanagerConfig.Resolved()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := resolved.AlertmanagerConfig()
|
||||
|
||||
var err error
|
||||
// Load SigNoz's alertmanager notification templates from the configured
|
||||
// globs. The upstream default templates (default.tmpl, email.tmpl) are
|
||||
// always loaded from the embedded alertmanager assets inside FromGlobs, so
|
||||
@@ -275,7 +278,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
|
||||
server.logger.InfoContext(ctx, "skipping creation of receiver not referenced by any route", slog.String("receiver", rcv.Name))
|
||||
continue
|
||||
}
|
||||
extendedRcv, err := alertmanagerConfig.GetReceiver(rcv.Name)
|
||||
extendedRcv, err := resolved.GetReceiver(rcv.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -350,7 +353,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
|
||||
go server.dispatcher.Run()
|
||||
go server.inhibitor.Run()
|
||||
|
||||
server.alertmanagerConfig = alertmanagerConfig
|
||||
server.alertmanagerConfig = resolved
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package alertmanager
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -26,11 +25,6 @@ type Signoz struct {
|
||||
alertmanagerserver.Config `mapstructure:",squash" yaml:",squash"`
|
||||
}
|
||||
|
||||
type Legacy struct {
|
||||
// ApiURL is the URL of the legacy signoz alertmanager.
|
||||
ApiURL *url.URL `mapstructure:"api_url"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
return factory.NewConfigFactory(factory.MustNewName("alertmanager"), newConfig)
|
||||
}
|
||||
|
||||
@@ -167,6 +167,10 @@ func (provider *provider) UpdateChannelByReceiverAndID(ctx context.Context, orgI
|
||||
return err
|
||||
}
|
||||
|
||||
if err := config.SetGlobalConfig(provider.config.Signoz.Global); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := config.UpdateReceiver(receiver); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -217,6 +221,10 @@ func (provider *provider) CreateChannel(ctx context.Context, orgID string, recei
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := config.SetGlobalConfig(provider.config.Signoz.Global); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := config.CreateReceiver(receiver); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -85,6 +85,23 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}/migrate", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.MigrateV2), handler.OpenAPIDef{
|
||||
ID: "MigrateDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Migrate dashboard to v2",
|
||||
Description: "This endpoint retries the v1→v2 (Perses) migration on a dashboard still stored in the v1 schema and returns the v2-shape result. It is idempotent: a dashboard already in the v2 schema is returned unchanged.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(dashboardtypes.GettableDashboardV2),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.dashboardHandler.GetV2), handler.OpenAPIDef{
|
||||
ID: "GetDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
|
||||
@@ -10,11 +10,8 @@ var (
|
||||
FeaturePutMetersInZeus = featuretypes.MustNewName("put_meters_in_zeus")
|
||||
FeatureUseMeterReporter = featuretypes.MustNewName("use_meter_reporter")
|
||||
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
|
||||
FeatureUseFineGrainedAuthz = featuretypes.MustNewName("use_fine_grained_authz")
|
||||
FeatureEnableAIObservability = featuretypes.MustNewName("enable_ai_observability")
|
||||
FeatureEnableMetricsReduction = featuretypes.MustNewName("enable_metrics_reduction")
|
||||
FeatureUseInfraMonitoringV2 = featuretypes.MustNewName("use_infra_monitoring_v2")
|
||||
|
||||
FeatureUsePrometheusClickhouseV2 = featuretypes.MustNewName("use_prometheus_clickhouse_v2")
|
||||
)
|
||||
|
||||
@@ -76,14 +73,6 @@ func MustNewRegistry() featuretypes.Registry {
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureUseFineGrainedAuthz,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
Stage: featuretypes.StageExperimental,
|
||||
Description: "Controls whether fine-grained authorization is enabled",
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureEnableAIObservability,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
@@ -100,14 +89,6 @@ func MustNewRegistry() featuretypes.Registry {
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureUseInfraMonitoringV2,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
Stage: featuretypes.StageExperimental,
|
||||
Description: "Controls whether infra monitoring v2 is enabled",
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureUsePrometheusClickhouseV2,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package implcloudintegration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
citypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServiceDefinitionsAreValid(t *testing.T) {
|
||||
store := NewServiceDefinitionStore()
|
||||
|
||||
for _, provider := range []citypes.CloudProviderType{
|
||||
citypes.CloudProviderTypeAWS,
|
||||
citypes.CloudProviderTypeAzure,
|
||||
citypes.CloudProviderTypeGCP,
|
||||
} {
|
||||
t.Run(provider.StringValue(), func(t *testing.T) {
|
||||
defs, err := store.List(context.Background(), provider)
|
||||
require.NoError(t, err, "all embedded definitions must load and validate")
|
||||
require.NotEmpty(t, defs, "provider should ship at least one service definition")
|
||||
|
||||
for _, def := range defs {
|
||||
assert.NotEmpty(t, def.ID, "service definition must have an id")
|
||||
assert.NotEmpty(t, def.Title, "service %q must have a title", def.ID)
|
||||
|
||||
// Get() must agree with List() for every service it advertises.
|
||||
serviceID, err := citypes.NewServiceID(provider, def.ID)
|
||||
if !assert.NoError(t, err, "service id %q must be registered in serviceid.go", def.ID) {
|
||||
continue
|
||||
}
|
||||
got, err := store.Get(context.Background(), provider, serviceID)
|
||||
require.NoError(t, err, "service %q listed but not gettable", def.ID)
|
||||
assert.Equal(t, def.ID, got.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -621,7 +621,7 @@
|
||||
{
|
||||
"metricName": "cloudsql.googleapis.com/database/postgresql/deadlock_count",
|
||||
"temporality": "",
|
||||
"timeAggregation": "max",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
@@ -882,7 +882,7 @@
|
||||
{
|
||||
"metricName": "cloudsql.googleapis.com/database/postgresql/transaction_count",
|
||||
"temporality": "",
|
||||
"timeAggregation": "max",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user