Compare commits

...

11 Commits

Author SHA1 Message Date
Abhi kumar
ab07ba6322 Merge branch 'fix/issue-6354' into chore/yaxis-cleanup 2026-02-23 18:37:26 +05:30
Abhi Kumar
5db92b46eb chore: minor change 2026-02-23 18:36:21 +05:30
Abhi Kumar
98c33d171c chore: cleaned up old yaxisselector 2026-02-23 18:31:36 +05:30
Abhi Kumar
cb6db1bfdf fix: fixed failing tests 2026-02-23 18:17:32 +05:30
Abhi Kumar
5133346f77 Merge branch 'main' of https://github.com/SigNoz/signoz into fix/issue-6354 2026-02-23 16:58:31 +05:30
Abhi Kumar
d48495aecc fix: fixed tsc 2026-02-23 16:58:14 +05:30
Vikrant Gupta
465e07de83 fix(openapi): make the error and status as mandatory (#10391)
* fix(openapi): make the error and status as mandatory

* fix(openapi): fix the frontend types
2026-02-23 16:47:24 +05:30
Abhi Kumar
53d7753167 fix: fixed unit converstion support across thresholds and yaxisunit 2026-02-23 16:32:25 +05:30
Vikrant Gupta
c04f664e2f fix(openapi): make the data and status required in success responses (#10390) 2026-02-23 16:15:36 +05:30
Abhi kumar
e406b0bb61 fix: formatting chart manager aggregation values with the yaxis unit (#10379)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix: formatting chart manager aggergation values with the yaxis unit

* chore: pr review changes

* chore: pr review changes
2026-02-23 06:23:26 +00:00
Nikhil Mantri
c143e0b130 chore: show warning and link to doc on missing hostname in infra tab (#10279) 2026-02-23 11:34:29 +05:30
33 changed files with 1408 additions and 1183 deletions

View File

@@ -326,6 +326,9 @@ components:
type: string
url:
type: string
required:
- code
- message
type: object
ErrorsResponseerroradditional:
properties:
@@ -1661,6 +1664,9 @@ components:
$ref: '#/components/schemas/ErrorsJSON'
status:
type: string
required:
- status
- error
type: object
RoletypesGettableResources:
properties:
@@ -2150,6 +2156,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"500":
@@ -2233,6 +2242,9 @@ paths:
$ref: '#/components/schemas/AuthtypesGettableToken'
status:
type: string
required:
- status
- data
type: object
description: See Other
"400":
@@ -2271,6 +2283,9 @@ paths:
$ref: '#/components/schemas/AuthtypesGettableToken'
status:
type: string
required:
- status
- data
type: object
description: See Other
"400":
@@ -2334,6 +2349,9 @@ paths:
$ref: '#/components/schemas/AuthtypesGettableToken'
status:
type: string
required:
- status
- data
type: object
description: See Other
"400":
@@ -2428,6 +2446,9 @@ paths:
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -2482,6 +2503,9 @@ paths:
$ref: '#/components/schemas/TypesIdentifiable'
status:
type: string
required:
- status
- data
type: object
description: Created
"401":
@@ -2575,6 +2599,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -2622,6 +2649,9 @@ paths:
$ref: '#/components/schemas/AuthtypesGettableAuthDomain'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -2818,6 +2848,9 @@ paths:
$ref: '#/components/schemas/TelemetrytypesGettableFieldKeys'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -2908,6 +2941,9 @@ paths:
$ref: '#/components/schemas/TelemetrytypesGettableFieldValues'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -2957,6 +2993,9 @@ paths:
$ref: '#/components/schemas/TypesResetPasswordToken'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -3012,6 +3051,9 @@ paths:
$ref: '#/components/schemas/TypesGettableGlobalConfig'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -3057,6 +3099,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -3104,6 +3149,9 @@ paths:
$ref: '#/components/schemas/TypesInvite'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
@@ -3217,6 +3265,9 @@ paths:
$ref: '#/components/schemas/TypesInvite'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -3260,6 +3311,9 @@ paths:
$ref: '#/components/schemas/TypesUser'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
@@ -3354,6 +3408,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -3452,6 +3509,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -3501,6 +3561,9 @@ paths:
$ref: '#/components/schemas/PreferencetypesPreference'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -3614,6 +3677,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -3661,6 +3727,9 @@ paths:
$ref: '#/components/schemas/TypesGettableAPIKey'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
@@ -3828,6 +3897,9 @@ paths:
$ref: '#/components/schemas/DashboardtypesGettablePublicDashboardData'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -3881,6 +3953,9 @@ paths:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -3958,6 +4033,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4005,6 +4083,9 @@ paths:
$ref: '#/components/schemas/TypesIdentifiable'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
@@ -4139,6 +4220,9 @@ paths:
$ref: '#/components/schemas/RoletypesRole'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4262,6 +4346,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4401,6 +4488,9 @@ paths:
$ref: '#/components/schemas/RoletypesGettableResources'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4446,6 +4536,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4540,6 +4633,9 @@ paths:
$ref: '#/components/schemas/TypesUser'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4599,6 +4695,9 @@ paths:
$ref: '#/components/schemas/TypesUser'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -4654,6 +4753,9 @@ paths:
$ref: '#/components/schemas/TypesUser'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4696,6 +4798,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4745,6 +4850,9 @@ paths:
$ref: '#/components/schemas/PreferencetypesPreference'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4887,6 +4995,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4939,6 +5050,9 @@ paths:
$ref: '#/components/schemas/GatewaytypesGettableIngestionKeys'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4986,6 +5100,9 @@ paths:
$ref: '#/components/schemas/GatewaytypesGettableCreatedIngestionKey'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -5124,6 +5241,9 @@ paths:
$ref: '#/components/schemas/GatewaytypesGettableCreatedIngestionKeyLimit'
status:
type: string
required:
- status
- data
type: object
description: Created
"401":
@@ -5265,6 +5385,9 @@ paths:
$ref: '#/components/schemas/GatewaytypesGettableIngestionKeys'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -5328,6 +5451,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesListMetricsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5383,6 +5509,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesMetricAlertsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5449,6 +5578,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesMetricAttributesResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5504,6 +5636,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboardsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5560,6 +5695,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesMetricHighlightsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5616,6 +5754,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesMetricMetadata'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5732,6 +5873,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesStatsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5787,6 +5931,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesTreemapResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5836,6 +5983,9 @@ paths:
$ref: '#/components/schemas/TypesOrganization'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -5966,6 +6116,9 @@ paths:
$ref: '#/components/schemas/AuthtypesSessionContext'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -6003,6 +6156,9 @@ paths:
$ref: '#/components/schemas/AuthtypesGettableToken'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -6046,6 +6202,9 @@ paths:
$ref: '#/components/schemas/AuthtypesGettableToken'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -6078,6 +6237,9 @@ paths:
$ref: '#/components/schemas/ZeustypesGettableHost'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -6592,6 +6754,9 @@ paths:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -6646,6 +6811,9 @@ paths:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeRequest'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":

View File

@@ -436,7 +436,7 @@ export interface ErrorsJSONDTO {
/**
* @type string
*/
code?: string;
code: string;
/**
* @type array
*/
@@ -444,7 +444,7 @@ export interface ErrorsJSONDTO {
/**
* @type string
*/
message?: string;
message: string;
/**
* @type string
*/
@@ -1985,11 +1985,11 @@ export enum Querybuildertypesv5VariableTypeDTO {
text = 'text',
}
export interface RenderErrorResponseDTO {
error?: ErrorsJSONDTO;
error: ErrorsJSONDTO;
/**
* @type string
*/
status?: string;
status: string;
}
/**
@@ -2599,30 +2599,30 @@ export type AuthzCheck200 = {
/**
* @type array
*/
data?: AuthtypesGettableTransactionDTO[];
data: AuthtypesGettableTransactionDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type ChangePasswordPathParameters = {
id: string;
};
export type CreateSessionByGoogleCallback303 = {
data?: AuthtypesGettableTokenDTO;
data: AuthtypesGettableTokenDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateSessionByOIDCCallback303 = {
data?: AuthtypesGettableTokenDTO;
data: AuthtypesGettableTokenDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateSessionBySAMLCallbackParams = {
@@ -2650,11 +2650,11 @@ export type CreateSessionBySAMLCallbackBody = {
};
export type CreateSessionBySAMLCallback303 = {
data?: AuthtypesGettableTokenDTO;
data: AuthtypesGettableTokenDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type DeletePublicDashboardPathParameters = {
@@ -2664,22 +2664,22 @@ export type GetPublicDashboardPathParameters = {
id: string;
};
export type GetPublicDashboard200 = {
data?: DashboardtypesGettablePublicDasbhboardDTO;
data: DashboardtypesGettablePublicDasbhboardDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type CreatePublicDashboardPathParameters = {
id: string;
};
export type CreatePublicDashboard201 = {
data?: TypesIdentifiableDTO;
data: TypesIdentifiableDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type UpdatePublicDashboardPathParameters = {
@@ -2689,19 +2689,19 @@ export type ListAuthDomains200 = {
/**
* @type array
*/
data?: AuthtypesGettableAuthDomainDTO[];
data: AuthtypesGettableAuthDomainDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateAuthDomain200 = {
data?: AuthtypesGettableAuthDomainDTO;
data: AuthtypesGettableAuthDomainDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type DeleteAuthDomainPathParameters = {
@@ -2757,11 +2757,11 @@ export type GetFieldsKeysParams = {
};
export type GetFieldsKeys200 = {
data?: TelemetrytypesGettableFieldKeysDTO;
data: TelemetrytypesGettableFieldKeysDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetFieldsValuesParams = {
@@ -2821,49 +2821,49 @@ export type GetFieldsValuesParams = {
};
export type GetFieldsValues200 = {
data?: TelemetrytypesGettableFieldValuesDTO;
data: TelemetrytypesGettableFieldValuesDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetResetPasswordTokenPathParameters = {
id: string;
};
export type GetResetPasswordToken200 = {
data?: TypesResetPasswordTokenDTO;
data: TypesResetPasswordTokenDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetGlobalConfig200 = {
data?: TypesGettableGlobalConfigDTO;
data: TypesGettableGlobalConfigDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type ListInvite200 = {
/**
* @type array
*/
data?: TypesInviteDTO[];
data: TypesInviteDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateInvite201 = {
data?: TypesInviteDTO;
data: TypesInviteDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type DeleteInvitePathParameters = {
@@ -2873,19 +2873,19 @@ export type GetInvitePathParameters = {
token: string;
};
export type GetInvite200 = {
data?: TypesInviteDTO;
data: TypesInviteDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type AcceptInvite201 = {
data?: TypesUserDTO;
data: TypesUserDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type ListPromotedAndIndexedPaths200 = {
@@ -2893,33 +2893,33 @@ export type ListPromotedAndIndexedPaths200 = {
* @type array
* @nullable true
*/
data?: PromotetypesPromotePathDTO[] | null;
data: PromotetypesPromotePathDTO[] | null;
/**
* @type string
*/
status?: string;
status: string;
};
export type ListOrgPreferences200 = {
/**
* @type array
*/
data?: PreferencetypesPreferenceDTO[];
data: PreferencetypesPreferenceDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type GetOrgPreferencePathParameters = {
name: string;
};
export type GetOrgPreference200 = {
data?: PreferencetypesPreferenceDTO;
data: PreferencetypesPreferenceDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type UpdateOrgPreferencePathParameters = {
@@ -2929,19 +2929,19 @@ export type ListAPIKeys200 = {
/**
* @type array
*/
data?: TypesGettableAPIKeyDTO[];
data: TypesGettableAPIKeyDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateAPIKey201 = {
data?: TypesGettableAPIKeyDTO;
data: TypesGettableAPIKeyDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type RevokeAPIKeyPathParameters = {
@@ -2954,11 +2954,11 @@ export type GetPublicDashboardDataPathParameters = {
id: string;
};
export type GetPublicDashboardData200 = {
data?: DashboardtypesGettablePublicDashboardDataDTO;
data: DashboardtypesGettablePublicDashboardDataDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetPublicDashboardWidgetQueryRangePathParameters = {
@@ -2966,30 +2966,30 @@ export type GetPublicDashboardWidgetQueryRangePathParameters = {
idx: string;
};
export type GetPublicDashboardWidgetQueryRange200 = {
data?: Querybuildertypesv5QueryRangeResponseDTO;
data: Querybuildertypesv5QueryRangeResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type ListRoles200 = {
/**
* @type array
*/
data?: RoletypesRoleDTO[];
data: RoletypesRoleDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateRole201 = {
data?: TypesIdentifiableDTO;
data: TypesIdentifiableDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type DeleteRolePathParameters = {
@@ -2999,11 +2999,11 @@ export type GetRolePathParameters = {
id: string;
};
export type GetRole200 = {
data?: RoletypesRoleDTO;
data: RoletypesRoleDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type PatchRolePathParameters = {
@@ -3017,11 +3017,11 @@ export type GetObjects200 = {
/**
* @type array
*/
data?: AuthtypesObjectDTO[];
data: AuthtypesObjectDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type PatchObjectsPathParameters = {
@@ -3029,22 +3029,22 @@ export type PatchObjectsPathParameters = {
relation: string;
};
export type GetResources200 = {
data?: RoletypesGettableResourcesDTO;
data: RoletypesGettableResourcesDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type ListUsers200 = {
/**
* @type array
*/
data?: TypesUserDTO[];
data: TypesUserDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type DeleteUserPathParameters = {
@@ -3054,52 +3054,52 @@ export type GetUserPathParameters = {
id: string;
};
export type GetUser200 = {
data?: TypesUserDTO;
data: TypesUserDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type UpdateUserPathParameters = {
id: string;
};
export type UpdateUser200 = {
data?: TypesUserDTO;
data: TypesUserDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMyUser200 = {
data?: TypesUserDTO;
data: TypesUserDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type ListUserPreferences200 = {
/**
* @type array
*/
data?: PreferencetypesPreferenceDTO[];
data: PreferencetypesPreferenceDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type GetUserPreferencePathParameters = {
name: string;
};
export type GetUserPreference200 = {
data?: PreferencetypesPreferenceDTO;
data: PreferencetypesPreferenceDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type UpdateUserPreferencePathParameters = {
@@ -3109,11 +3109,11 @@ export type GetFeatures200 = {
/**
* @type array
*/
data?: FeaturetypesGettableFeatureDTO[];
data: FeaturetypesGettableFeatureDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type GetIngestionKeysParams = {
@@ -3130,19 +3130,19 @@ export type GetIngestionKeysParams = {
};
export type GetIngestionKeys200 = {
data?: GatewaytypesGettableIngestionKeysDTO;
data: GatewaytypesGettableIngestionKeysDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateIngestionKey200 = {
data?: GatewaytypesGettableCreatedIngestionKeyDTO;
data: GatewaytypesGettableCreatedIngestionKeyDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type DeleteIngestionKeyPathParameters = {
@@ -3155,11 +3155,11 @@ export type CreateIngestionKeyLimitPathParameters = {
keyId: string;
};
export type CreateIngestionKeyLimit201 = {
data?: GatewaytypesGettableCreatedIngestionKeyLimitDTO;
data: GatewaytypesGettableCreatedIngestionKeyLimitDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type DeleteIngestionKeyLimitPathParameters = {
@@ -3187,11 +3187,11 @@ export type SearchIngestionKeysParams = {
};
export type SearchIngestionKeys200 = {
data?: GatewaytypesGettableIngestionKeysDTO;
data: GatewaytypesGettableIngestionKeysDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type ListMetricsParams = {
@@ -3220,22 +3220,22 @@ export type ListMetricsParams = {
};
export type ListMetrics200 = {
data?: MetricsexplorertypesListMetricsResponseDTO;
data: MetricsexplorertypesListMetricsResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMetricAlertsPathParameters = {
metricName: string;
};
export type GetMetricAlerts200 = {
data?: MetricsexplorertypesMetricAlertsResponseDTO;
data: MetricsexplorertypesMetricAlertsResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMetricAttributesPathParameters = {
@@ -3257,117 +3257,117 @@ export type GetMetricAttributesParams = {
};
export type GetMetricAttributes200 = {
data?: MetricsexplorertypesMetricAttributesResponseDTO;
data: MetricsexplorertypesMetricAttributesResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMetricDashboardsPathParameters = {
metricName: string;
};
export type GetMetricDashboards200 = {
data?: MetricsexplorertypesMetricDashboardsResponseDTO;
data: MetricsexplorertypesMetricDashboardsResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMetricHighlightsPathParameters = {
metricName: string;
};
export type GetMetricHighlights200 = {
data?: MetricsexplorertypesMetricHighlightsResponseDTO;
data: MetricsexplorertypesMetricHighlightsResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMetricMetadataPathParameters = {
metricName: string;
};
export type GetMetricMetadata200 = {
data?: MetricsexplorertypesMetricMetadataDTO;
data: MetricsexplorertypesMetricMetadataDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type UpdateMetricMetadataPathParameters = {
metricName: string;
};
export type GetMetricsStats200 = {
data?: MetricsexplorertypesStatsResponseDTO;
data: MetricsexplorertypesStatsResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMetricsTreemap200 = {
data?: MetricsexplorertypesTreemapResponseDTO;
data: MetricsexplorertypesTreemapResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMyOrganization200 = {
data?: TypesOrganizationDTO;
data: TypesOrganizationDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetSessionContext200 = {
data?: AuthtypesSessionContextDTO;
data: AuthtypesSessionContextDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateSessionByEmailPassword200 = {
data?: AuthtypesGettableTokenDTO;
data: AuthtypesGettableTokenDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type RotateSession200 = {
data?: AuthtypesGettableTokenDTO;
data: AuthtypesGettableTokenDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetHosts200 = {
data?: ZeustypesGettableHostDTO;
data: ZeustypesGettableHostDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type QueryRangeV5200 = {
data?: Querybuildertypesv5QueryRangeResponseDTO;
data: Querybuildertypesv5QueryRangeResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type ReplaceVariables200 = {
data?: Querybuildertypesv5QueryRangeRequestDTO;
data: Querybuildertypesv5QueryRangeRequestDTO;
/**
* @type string
*/
status?: string;
status: string;
};

View File

@@ -1,4 +1,5 @@
import { UniversalYAxisUnit } from '../types';
import { YAxisCategoryNames } from '../constants';
import { UniversalYAxisUnit, YAxisCategory } from '../types';
import {
getUniversalNameFromMetricUnit,
mapMetricUnitToUniversalUnit,
@@ -41,29 +42,29 @@ describe('YAxisUnitSelector utils', () => {
describe('mergeCategories', () => {
it('merges categories correctly', () => {
const categories1 = [
const categories1: YAxisCategory[] = [
{
name: 'Data',
name: YAxisCategoryNames.Data,
units: [
{ name: 'bytes', id: UniversalYAxisUnit.BYTES },
{ name: 'kilobytes', id: UniversalYAxisUnit.KILOBYTES },
],
},
];
const categories2 = [
const categories2: YAxisCategory[] = [
{
name: 'Data',
name: YAxisCategoryNames.Data,
units: [{ name: 'bits', id: UniversalYAxisUnit.BITS }],
},
{
name: 'Time',
name: YAxisCategoryNames.Time,
units: [{ name: 'seconds', id: UniversalYAxisUnit.SECONDS }],
},
];
const mergedCategories = mergeCategories(categories1, categories2);
expect(mergedCategories).toEqual([
{
name: 'Data',
name: YAxisCategoryNames.Data,
units: [
{ name: 'bytes', id: UniversalYAxisUnit.BYTES },
{ name: 'kilobytes', id: UniversalYAxisUnit.KILOBYTES },
@@ -71,7 +72,7 @@ describe('YAxisUnitSelector utils', () => {
],
},
{
name: 'Time',
name: YAxisCategoryNames.Time,
units: [{ name: 'seconds', id: UniversalYAxisUnit.SECONDS }],
},
]);

View File

@@ -1,5 +1,36 @@
import { UnitFamilyConfig, UniversalYAxisUnit, YAxisUnit } from './types';
export enum YAxisCategoryNames {
Time = 'Time',
Data = 'Data',
DataRate = 'Data Rate',
Count = 'Count',
Operations = 'Operations',
Percentage = 'Percentage',
Boolean = 'Boolean',
None = 'None',
HashRate = 'Hash Rate',
Miscellaneous = 'Miscellaneous',
Acceleration = 'Acceleration',
Angular = 'Angular',
Area = 'Area',
Flops = 'FLOPs',
Concentration = 'Concentration',
Currency = 'Currency',
Datetime = 'Datetime',
PowerElectrical = 'Power/Electrical',
Flow = 'Flow',
Force = 'Force',
Mass = 'Mass',
Length = 'Length',
Pressure = 'Pressure',
Radiation = 'Radiation',
RotationSpeed = 'Rotation Speed',
Temperature = 'Temperature',
Velocity = 'Velocity',
Volume = 'Volume',
}
// Mapping of universal y-axis units to their AWS, UCUM, and OpenMetrics equivalents (if available)
export const UniversalYAxisUnitMappings: Partial<
Record<UniversalYAxisUnit, Set<YAxisUnit> | null>

View File

@@ -1,10 +1,11 @@
import { Y_AXIS_UNIT_NAMES } from './constants';
import { YAxisCategoryNames } from './constants';
import { UniversalYAxisUnit, YAxisCategory } from './types';
// Base categories for the universal y-axis units
export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
{
name: 'Time',
name: YAxisCategoryNames.Time,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.SECONDS],
@@ -37,7 +38,7 @@ export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Data',
name: YAxisCategoryNames.Data,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.BYTES],
@@ -154,7 +155,7 @@ export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Data Rate',
name: YAxisCategoryNames.DataRate,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.BYTES_SECOND],
@@ -295,7 +296,7 @@ export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Count',
name: YAxisCategoryNames.Count,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.COUNT],
@@ -312,7 +313,7 @@ export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Operations',
name: YAxisCategoryNames.Operations,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.OPS_SECOND],
@@ -353,7 +354,7 @@ export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Percentage',
name: YAxisCategoryNames.Percentage,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.PERCENT],
@@ -366,7 +367,7 @@ export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Boolean',
name: YAxisCategoryNames.Boolean,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.TRUE_FALSE],
@@ -382,7 +383,7 @@ export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
{
name: 'Time',
name: YAxisCategoryNames.Time,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.DURATION_MS],
@@ -419,7 +420,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Data Rate',
name: YAxisCategoryNames.DataRate,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.DATA_RATE_PACKETS_PER_SECOND],
@@ -428,7 +429,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Boolean',
name: YAxisCategoryNames.Boolean,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.ON_OFF],
@@ -437,7 +438,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'None',
name: YAxisCategoryNames.None,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.NONE],
@@ -446,7 +447,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Hash Rate',
name: YAxisCategoryNames.HashRate,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.HASH_RATE_HASHES_PER_SECOND],
@@ -479,7 +480,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Miscellaneous',
name: YAxisCategoryNames.Miscellaneous,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MISC_STRING],
@@ -520,7 +521,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Acceleration',
name: YAxisCategoryNames.Acceleration,
units: [
{
name:
@@ -541,7 +542,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Angular',
name: YAxisCategoryNames.Angular,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.ANGULAR_DEGREE],
@@ -566,7 +567,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Area',
name: YAxisCategoryNames.Area,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.AREA_SQUARE_METERS],
@@ -583,7 +584,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'FLOPs',
name: YAxisCategoryNames.Flops,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.FLOPS_FLOPS],
@@ -620,7 +621,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Concentration',
name: YAxisCategoryNames.Concentration,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.CONCENTRATION_PPM],
@@ -677,7 +678,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Currency',
name: YAxisCategoryNames.Currency,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.CURRENCY_USD],
@@ -774,7 +775,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Datetime',
name: YAxisCategoryNames.Datetime,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.DATETIME_ISO],
@@ -811,7 +812,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Power/Electrical',
name: YAxisCategoryNames.PowerElectrical,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.POWER_WATT],
@@ -968,7 +969,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Flow',
name: YAxisCategoryNames.Flow,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.FLOW_GALLONS_PER_MINUTE],
@@ -1005,7 +1006,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Force',
name: YAxisCategoryNames.Force,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.FORCE_NEWTON_METERS],
@@ -1026,7 +1027,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Mass',
name: YAxisCategoryNames.Mass,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MASS_MILLIGRAM],
@@ -1051,7 +1052,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Length',
name: YAxisCategoryNames.Length,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.LENGTH_MILLIMETER],
@@ -1080,7 +1081,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Pressure',
name: YAxisCategoryNames.Pressure,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.PRESSURE_MILLIBAR],
@@ -1117,7 +1118,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Radiation',
name: YAxisCategoryNames.Radiation,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.RADIATION_BECQUEREL],
@@ -1174,7 +1175,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Rotation Speed',
name: YAxisCategoryNames.RotationSpeed,
units: [
{
name:
@@ -1200,7 +1201,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Temperature',
name: YAxisCategoryNames.Temperature,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.TEMPERATURE_CELSIUS],
@@ -1217,7 +1218,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Velocity',
name: YAxisCategoryNames.Velocity,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.VELOCITY_METERS_PER_SECOND],
@@ -1238,7 +1239,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Volume',
name: YAxisCategoryNames.Volume,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.VOLUME_MILLILITER],

View File

@@ -1,3 +1,5 @@
import { YAxisCategoryNames } from './constants';
export interface YAxisUnitSelectorProps {
value: string | undefined;
onChange: (value: UniversalYAxisUnit) => void;
@@ -669,7 +671,7 @@ export interface UnitFamilyConfig {
}
export interface YAxisCategory {
name: string;
name: YAxisCategoryNames;
units: {
name: string;
id: UniversalYAxisUnit;

View File

@@ -1,3 +1,22 @@
.chart-manager-series-label {
width: 100%;
min-width: 0;
max-width: 100%;
cursor: pointer;
border: none;
background-color: transparent;
color: inherit;
text-align: left;
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:disabled {
cursor: not-allowed;
}
}
.chart-manager-container {
width: 100%;
max-height: calc(40% - 40px);

View File

@@ -1,24 +1,28 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Button, Input } from 'antd';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import { ResizeTable } from 'components/ResizeTable';
import { getGraphManagerTableColumns } from 'container/GridCardLayout/GridCard/FullView/TableRender/GraphManagerColumns';
import { ExtendedChartDataset } from 'container/GridCardLayout/GridCard/FullView/types';
import { getDefaultTableDataSet } from 'container/GridCardLayout/GridCard/FullView/utils';
import { useNotifications } from 'hooks/useNotifications';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { usePlotContext } from 'lib/uPlotV2/context/PlotContext';
import useLegendsSync from 'lib/uPlotV2/hooks/useLegendsSync';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { getChartManagerColumns } from './getChartMangerColumns';
import { ExtendedChartDataset, getDefaultTableDataSet } from './utils';
import './ChartManager.styles.scss';
interface ChartManagerProps {
config: UPlotConfigBuilder;
alignedData: uPlot.AlignedData;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;
onCancel?: () => void;
}
const X_AXIS_INDEX = 0;
/**
* ChartManager provides a tabular view to manage the visibility of
* individual series on a uPlot chart.
@@ -28,16 +32,12 @@ interface ChartManagerProps {
* - filter series by label
* - toggle individual series on/off
* - persist the visibility configuration to local storage.
*
* @param config - `UPlotConfigBuilder` instance used to derive chart options.
* @param alignedData - uPlot aligned data used to build the initial table dataset.
* @param yAxisUnit - Optional unit label for Y-axis values shown in the table.
* @param onCancel - Optional callback invoked when the user cancels the dialog.
*/
export default function ChartManager({
config,
alignedData,
yAxisUnit,
decimalPrecision = PrecisionOptionsEnum.TWO,
onCancel,
}: ChartManagerProps): JSX.Element {
const { notifications } = useNotifications();
@@ -53,8 +53,13 @@ export default function ChartManager({
const { isDashboardLocked } = useDashboard();
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(() =>
getDefaultTableDataSet(config.getConfig() as uPlot.Options, alignedData),
getDefaultTableDataSet(
config.getConfig() as uPlot.Options,
alignedData,
decimalPrecision,
),
);
const [filterValue, setFilterValue] = useState('');
const graphVisibilityState = useMemo(
() =>
@@ -67,46 +72,62 @@ export default function ChartManager({
useEffect(() => {
setTableDataSet(
getDefaultTableDataSet(config.getConfig() as uPlot.Options, alignedData),
);
}, [alignedData, config]);
const filterHandler = useCallback(
(event: React.ChangeEvent<HTMLInputElement>): void => {
const value = event.target.value.toString().toLowerCase();
const updatedDataSet = tableDataSet.map((item) => {
if (item.label?.toLocaleLowerCase().includes(value)) {
return { ...item, show: true };
}
return { ...item, show: false };
});
setTableDataSet(updatedDataSet);
},
[tableDataSet],
);
const dataSource = useMemo(
() =>
tableDataSet.filter(
(item, index) => index !== 0 && item.show, // skipping the first item as it is the x-axis
getDefaultTableDataSet(
config.getConfig() as uPlot.Options,
alignedData,
decimalPrecision,
),
[tableDataSet],
);
setFilterValue('');
}, [alignedData, config, decimalPrecision]);
const handleFilterChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
setFilterValue(e.target.value.toLowerCase());
},
[],
);
const handleToggleSeriesOnOff = useCallback(
(index: number): void => {
onToggleSeriesOnOff(index);
},
[onToggleSeriesOnOff],
);
const dataSource = useMemo(() => {
const filter = filterValue.trim();
return tableDataSet.filter((item, index) => {
if (index === X_AXIS_INDEX) {
return false;
}
if (!filter) {
return true;
}
return item.label?.toLowerCase().includes(filter) ?? false;
});
}, [tableDataSet, filterValue]);
const columns = useMemo(
() =>
getGraphManagerTableColumns({
getChartManagerColumns({
tableDataSet,
checkBoxOnChangeHandler: (_e, index) => {
onToggleSeriesOnOff(index);
},
graphVisibilityState,
labelClickedHandler: onToggleSeriesVisibility,
onToggleSeriesOnOff: handleToggleSeriesOnOff,
onToggleSeriesVisibility,
yAxisUnit,
isGraphDisabled: isDashboardLocked,
decimalPrecision,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[tableDataSet, graphVisibilityState, yAxisUnit, isDashboardLocked],
[
tableDataSet,
graphVisibilityState,
handleToggleSeriesOnOff,
onToggleSeriesVisibility,
yAxisUnit,
isDashboardLocked,
decimalPrecision,
],
);
const handleSave = useCallback((): void => {
@@ -114,15 +135,18 @@ export default function ChartManager({
notifications.success({
message: 'The updated graphs & legends are saved',
});
if (onCancel) {
onCancel();
}
onCancel?.();
}, [syncSeriesVisibilityToLocalStorage, notifications, onCancel]);
return (
<div className="chart-manager-container">
<div className="chart-manager-header">
<Input onChange={filterHandler} placeholder="Filter Series" />
<Input
placeholder="Filter Series"
value={filterValue}
onChange={handleFilterChange}
data-testid="filter-input"
/>
<div className="chart-manager-actions-container">
<Button type="default" onClick={onCancel}>
Cancel
@@ -136,10 +160,10 @@ export default function ChartManager({
<ResizeTable
columns={columns}
dataSource={dataSource}
virtual
rowKey="index"
scroll={{ y: 200 }}
pagination={false}
virtual
/>
</div>
</div>

View File

@@ -0,0 +1,31 @@
import { Tooltip } from 'antd';
import './ChartManager.styles.scss';
interface SeriesLabelProps {
label: string;
labelIndex: number;
onClick: (idx: number) => void;
disabled?: boolean;
}
export function SeriesLabel({
label,
labelIndex,
onClick,
disabled,
}: SeriesLabelProps): JSX.Element {
return (
<Tooltip placement="topLeft" title={label}>
<button
className="chart-manager-series-label"
disabled={disabled}
type="button"
data-testid={`series-label-button-${labelIndex}`}
onClick={(): void => onClick(labelIndex)}
>
{label}
</button>
</Tooltip>
);
}

View File

@@ -0,0 +1,172 @@
import userEvent from '@testing-library/user-event';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { render, screen } from 'tests/test-utils';
import ChartManager from '../ChartManager';
const mockSyncSeriesVisibilityToLocalStorage = jest.fn();
const mockNotificationsSuccess = jest.fn();
jest.mock('lib/uPlotV2/context/PlotContext', () => ({
usePlotContext: (): {
onToggleSeriesOnOff: jest.Mock;
onToggleSeriesVisibility: jest.Mock;
syncSeriesVisibilityToLocalStorage: jest.Mock;
} => ({
onToggleSeriesOnOff: jest.fn(),
onToggleSeriesVisibility: jest.fn(),
syncSeriesVisibilityToLocalStorage: mockSyncSeriesVisibilityToLocalStorage,
}),
}));
jest.mock('lib/uPlotV2/hooks/useLegendsSync', () => ({
__esModule: true,
default: (): {
legendItemsMap: { [key: number]: { show: boolean; label: string } };
} => ({
legendItemsMap: {
0: { show: true, label: 'Time' },
1: { show: true, label: 'Series 1' },
2: { show: true, label: 'Series 2' },
},
}),
}));
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): { isDashboardLocked: boolean } => ({
isDashboardLocked: false,
}),
}));
jest.mock('hooks/useNotifications', () => ({
useNotifications: (): { notifications: { success: jest.Mock } } => ({
notifications: {
success: mockNotificationsSuccess,
},
}),
}));
jest.mock('components/ResizeTable', () => {
const MockTable = ({
dataSource,
columns,
}: {
dataSource: { index: number; label?: string }[];
columns: { key: string; title: string }[];
}): JSX.Element => (
<div data-testid="resize-table">
{columns.map((col) => (
<span key={col.key}>{col.title}</span>
))}
{dataSource.map((row) => (
<div key={row.index} data-testid={`row-${row.index}`}>
{row.label}
</div>
))}
</div>
);
return { ResizeTable: MockTable };
});
const createMockConfig = (): { getConfig: () => uPlot.Options } => ({
getConfig: (): uPlot.Options => ({
width: 100,
height: 100,
series: [
{ label: 'Time', value: 'time' },
{ label: 'Series 1', scale: 'y' },
{ label: 'Series 2', scale: 'y' },
],
}),
});
const createAlignedData = (): uPlot.AlignedData => [
[1000, 2000, 3000],
[10, 20, 30],
[1, 2, 3],
];
describe('ChartManager', () => {
const mockOnCancel = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('renders filter input and action buttons', () => {
render(
<ChartManager
config={createMockConfig() as any}
alignedData={createAlignedData()}
onCancel={mockOnCancel}
/>,
);
expect(screen.getByPlaceholderText('Filter Series')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Cancel/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Save/ })).toBeInTheDocument();
});
it('renders ResizeTable with data', () => {
render(
<ChartManager
config={createMockConfig() as UPlotConfigBuilder}
alignedData={createAlignedData()}
/>,
);
expect(screen.getByTestId('resize-table')).toBeInTheDocument();
});
it('calls onCancel when Cancel button is clicked', async () => {
render(
<ChartManager
config={createMockConfig() as UPlotConfigBuilder}
alignedData={createAlignedData()}
onCancel={mockOnCancel}
/>,
);
await userEvent.click(screen.getByRole('button', { name: /Cancel/ }));
expect(mockOnCancel).toHaveBeenCalledTimes(1);
});
it('filters table data when typing in filter input', async () => {
render(
<ChartManager
config={createMockConfig() as UPlotConfigBuilder}
alignedData={createAlignedData()}
/>,
);
// Before filter: both Series 1 and Series 2 rows are visible
expect(screen.getByTestId('row-1')).toBeInTheDocument();
expect(screen.getByTestId('row-2')).toBeInTheDocument();
const filterInput = screen.getByTestId('filter-input');
await userEvent.type(filterInput, 'Series 1');
// After filter: only Series 1 row is visible, Series 2 row is filtered out
expect(screen.getByTestId('row-1')).toBeInTheDocument();
expect(screen.queryByTestId('row-2')).not.toBeInTheDocument();
});
it('calls syncSeriesVisibilityToLocalStorage, notifications.success, and onCancel when Save is clicked', async () => {
render(
<ChartManager
config={createMockConfig() as UPlotConfigBuilder}
alignedData={createAlignedData()}
onCancel={mockOnCancel}
/>,
);
await userEvent.click(screen.getByRole('button', { name: /Save/ }));
expect(mockSyncSeriesVisibilityToLocalStorage).toHaveBeenCalledTimes(1);
expect(mockNotificationsSuccess).toHaveBeenCalledWith({
message: 'The updated graphs & legends are saved',
});
expect(mockOnCancel).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,39 @@
import userEvent from '@testing-library/user-event';
import { render, screen } from 'tests/test-utils';
import { SeriesLabel } from '../SeriesLabel';
describe('SeriesLabel', () => {
it('renders the label text', () => {
render(
<SeriesLabel label="Test Series Label" labelIndex={1} onClick={jest.fn()} />,
);
expect(screen.getByTestId('series-label-button-1')).toHaveTextContent(
'Test Series Label',
);
});
it('calls onClick with labelIndex when clicked', async () => {
const onClick = jest.fn();
render(<SeriesLabel label="Series A" labelIndex={2} onClick={onClick} />);
await userEvent.click(screen.getByTestId('series-label-button-2'));
expect(onClick).toHaveBeenCalledWith(2);
expect(onClick).toHaveBeenCalledTimes(1);
});
it('renders disabled button when disabled prop is true', () => {
render(
<SeriesLabel label="Disabled" labelIndex={0} onClick={jest.fn()} disabled />,
);
const button = screen.getByTestId('series-label-button-0');
expect(button).toBeDisabled();
});
it('has chart-manager-series-label class', () => {
render(<SeriesLabel label="Label" labelIndex={0} onClick={jest.fn()} />);
const button = screen.getByTestId('series-label-button-0');
expect(button).toHaveClass('chart-manager-series-label');
});
});

View File

@@ -0,0 +1,167 @@
import { render } from '@testing-library/react';
import { Y_AXIS_UNIT_NAMES } from 'components/YAxisUnitSelector/constants';
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
import { getChartManagerColumns } from '../getChartMangerColumns';
import { ExtendedChartDataset } from '../utils';
const createMockDataset = (
index: number,
overrides: Partial<ExtendedChartDataset> = {},
): ExtendedChartDataset =>
({
index,
label: `Series ${index}`,
show: true,
sum: 100,
avg: 50,
min: 10,
max: 90,
stroke: '#ff0000',
...overrides,
} as ExtendedChartDataset);
describe('getChartManagerColumns', () => {
const tableDataSet: ExtendedChartDataset[] = [
createMockDataset(0, { label: 'Time' }),
createMockDataset(1),
createMockDataset(2),
];
const graphVisibilityState = [true, true, false];
const onToggleSeriesOnOff = jest.fn();
const onToggleSeriesVisibility = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('returns columns with expected structure', () => {
const columns = getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
});
expect(columns).toHaveLength(6);
expect(columns[0].key).toBe('index');
expect(columns[1].key).toBe('label');
expect(columns[2].key).toBe('avg');
expect(columns[3].key).toBe('sum');
expect(columns[4].key).toBe('max');
expect(columns[5].key).toBe('min');
});
it('includes Label column with title', () => {
const columns = getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
});
const labelCol = columns.find((c) => c.key === 'label');
expect(labelCol!.title).toBe('Label');
});
it('formats column titles with yAxisUnit', () => {
const columns = getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
yAxisUnit: 'ms',
});
const avgCol = columns.find((c) => c.key === 'avg');
expect(avgCol!.title).toBe(
`Avg (in ${Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MILLISECONDS]})`,
);
});
it('numeric column render returns formatted string with yAxisUnit', () => {
const columns = getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
yAxisUnit: 'ms',
});
const avgCol = columns.find((c) => c.key === 'avg');
const renderFn = avgCol?.render as
| ((val: number, record: ExtendedChartDataset, index: number) => string)
| undefined;
expect(renderFn).toBeDefined();
const output = renderFn!(123.45, tableDataSet[1], 1);
expect(output).toBe('123.45 ms');
});
it('numeric column render formats zero when value is undefined', () => {
const columns = getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
yAxisUnit: 'none',
});
const sumCol = columns.find((c) => c.key === 'sum');
const renderFn = sumCol?.render as
| ((
val: number | undefined,
record: ExtendedChartDataset,
index: number,
) => string)
| undefined;
const output = renderFn!(undefined, tableDataSet[1], 1);
expect(output).toBe('0');
});
it('label column render displays label text and is clickable', () => {
const columns = getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
});
const labelCol = columns.find((c) => c.key === 'label');
const renderFn = labelCol!.render as
| ((
label: string,
record: ExtendedChartDataset,
index: number,
) => JSX.Element)
| undefined;
expect(renderFn).toBeDefined();
const renderResult = renderFn!('Series 1', tableDataSet[1], 1);
const { getByRole } = render(renderResult);
expect(getByRole('button', { name: 'Series 1' })).toBeInTheDocument();
});
it('index column render renders checkbox with correct checked state', () => {
const columns = getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
});
const indexCol = columns.find((c) => c.key === 'index');
const renderFn = indexCol!.render as
| ((
_val: unknown,
record: ExtendedChartDataset,
index: number,
) => JSX.Element)
| undefined;
expect(renderFn).toBeDefined();
const { container } = render(renderFn!(null, tableDataSet[1], 1));
const checkbox = container.querySelector('input[type="checkbox"]');
expect(checkbox).toBeInTheDocument();
expect(checkbox).toBeChecked(); // graphVisibilityState[1] is true
});
});

View File

@@ -0,0 +1,113 @@
import { PrecisionOptionsEnum } from 'components/Graph/types';
import {
formatTableValueWithUnit,
getDefaultTableDataSet,
getTableColumnTitle,
} from '../utils';
describe('ChartManager utils', () => {
describe('getDefaultTableDataSet', () => {
const createOptions = (seriesCount: number): uPlot.Options => ({
series: Array.from({ length: seriesCount }, (_, i) =>
i === 0
? { label: 'Time', value: 'time' }
: { label: `Series ${i}`, scale: 'y' },
),
width: 100,
height: 100,
});
it('returns one row per series with computed stats', () => {
const options = createOptions(3);
const data: uPlot.AlignedData = [
[1000, 2000, 3000],
[10, 20, 30],
[1, 2, 3],
];
const result = getDefaultTableDataSet(options, data);
expect(result).toHaveLength(3);
expect(result[0]).toMatchObject({
index: 0,
label: 'Time',
show: true,
});
expect(result[1]).toMatchObject({
index: 1,
label: 'Series 1',
show: true,
sum: 60,
avg: 20,
max: 30,
min: 10,
});
expect(result[2]).toMatchObject({
index: 2,
label: 'Series 2',
show: true,
sum: 6,
avg: 2,
max: 3,
min: 1,
});
});
it('handles empty data arrays', () => {
const options = createOptions(2);
const data: uPlot.AlignedData = [[], []];
const result = getDefaultTableDataSet(options, data);
expect(result[0]).toMatchObject({
sum: 0,
avg: 0,
max: 0,
min: 0,
});
});
it('respects decimalPrecision parameter', () => {
const options = createOptions(2);
const data: uPlot.AlignedData = [[1000], [123.454]];
const resultTwo = getDefaultTableDataSet(
options,
data,
PrecisionOptionsEnum.TWO,
);
expect(resultTwo[1].avg).toBe(123.45);
const resultZero = getDefaultTableDataSet(
options,
data,
PrecisionOptionsEnum.ZERO,
);
expect(resultZero[1].avg).toBe(123);
});
});
describe('formatTableValueWithUnit', () => {
it('formats value with unit', () => {
const result = formatTableValueWithUnit(1234.56, 'ms');
expect(result).toBe('1.23 s');
});
it('falls back to none format when yAxisUnit is undefined', () => {
const result = formatTableValueWithUnit(123.45);
expect(result).toBe('123.45');
});
});
describe('getTableColumnTitle', () => {
it('returns title only when yAxisUnit is undefined', () => {
expect(getTableColumnTitle('Avg')).toBe('Avg');
});
it('returns title with unit when yAxisUnit is provided', () => {
const result = getTableColumnTitle('Avg', 'ms');
expect(result).toBe('Avg (in Milliseconds (ms))');
});
});
});

View File

@@ -0,0 +1,94 @@
import { ColumnType } from 'antd/es/table';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import CustomCheckBox from 'container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox';
import { SeriesLabel } from './SeriesLabel';
import {
ExtendedChartDataset,
formatTableValueWithUnit,
getTableColumnTitle,
} from './utils';
export interface GetChartManagerColumnsParams {
tableDataSet: ExtendedChartDataset[];
graphVisibilityState: boolean[];
onToggleSeriesOnOff: (index: number) => void;
onToggleSeriesVisibility: (index: number) => void;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;
isGraphDisabled?: boolean;
}
export function getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
yAxisUnit,
decimalPrecision = PrecisionOptionsEnum.TWO,
isGraphDisabled,
}: GetChartManagerColumnsParams): ColumnType<ExtendedChartDataset>[] {
return [
{
title: '',
width: 50,
dataIndex: 'index',
key: 'index',
render: (_: unknown, record: ExtendedChartDataset): JSX.Element => (
<CustomCheckBox
data={tableDataSet}
graphVisibilityState={graphVisibilityState}
index={record.index}
disabled={isGraphDisabled}
checkBoxOnChangeHandler={(_e, idx): void => onToggleSeriesOnOff(idx)}
/>
),
},
{
title: 'Label',
width: 300,
dataIndex: 'label',
key: 'label',
render: (label: string, record: ExtendedChartDataset): JSX.Element => (
<SeriesLabel
label={label ?? ''}
labelIndex={record.index}
disabled={isGraphDisabled}
onClick={onToggleSeriesVisibility}
/>
),
},
{
title: getTableColumnTitle('Avg', yAxisUnit),
width: 90,
dataIndex: 'avg',
key: 'avg',
render: (val: number | undefined): string =>
formatTableValueWithUnit(val ?? 0, yAxisUnit, decimalPrecision),
},
{
title: getTableColumnTitle('Sum', yAxisUnit),
width: 90,
dataIndex: 'sum',
key: 'sum',
render: (val: number | undefined): string =>
formatTableValueWithUnit(val ?? 0, yAxisUnit, decimalPrecision),
},
{
title: getTableColumnTitle('Max', yAxisUnit),
width: 90,
dataIndex: 'max',
key: 'max',
render: (val: number | undefined): string =>
formatTableValueWithUnit(val ?? 0, yAxisUnit, decimalPrecision),
},
{
title: getTableColumnTitle('Min', yAxisUnit),
width: 90,
dataIndex: 'min',
key: 'min',
render: (val: number | undefined): string =>
formatTableValueWithUnit(val ?? 0, yAxisUnit, decimalPrecision),
},
];
}

View File

@@ -0,0 +1,93 @@
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { Y_AXIS_UNIT_NAMES } from 'components/YAxisUnitSelector/constants';
import uPlot from 'uplot';
/** Extended series with computed stats for table display */
export type ExtendedChartDataset = uPlot.Series & {
show: boolean;
sum: number;
avg: number;
min: number;
max: number;
index: number;
};
function roundToDecimalPrecision(
value: number,
decimalPrecision: PrecisionOption = PrecisionOptionsEnum.TWO,
): number {
if (
typeof value !== 'number' ||
Number.isNaN(value) ||
value === Infinity ||
value === -Infinity
) {
return 0;
}
if (decimalPrecision === PrecisionOptionsEnum.FULL) {
return value;
}
// regex to match the decimal precision for the given decimal precision
const regex = new RegExp(`^-?\\d*\\.?0*\\d{0,${decimalPrecision}}`);
const matched = value ? value.toFixed(decimalPrecision).match(regex) : null;
return matched ? parseFloat(matched[0]) : 0;
}
/** Build table dataset from uPlot options and aligned data */
export function getDefaultTableDataSet(
options: uPlot.Options,
data: uPlot.AlignedData,
decimalPrecision: PrecisionOption = PrecisionOptionsEnum.TWO,
): ExtendedChartDataset[] {
return options.series.map(
(series: uPlot.Series, index: number): ExtendedChartDataset => {
const arr = (data[index] as number[]) ?? [];
const sum = arr.reduce((a, b) => a + b, 0) || 0;
const count = arr.length || 1;
const hasValues = arr.length > 0;
return {
...series,
index,
show: true,
sum: roundToDecimalPrecision(sum, decimalPrecision),
avg: roundToDecimalPrecision(sum / count, decimalPrecision),
max: hasValues
? roundToDecimalPrecision(Math.max(...arr), decimalPrecision)
: 0,
min: hasValues
? roundToDecimalPrecision(Math.min(...arr), decimalPrecision)
: 0,
};
},
);
}
/** Format numeric value for table display using yAxisUnit */
export function formatTableValueWithUnit(
value: number,
yAxisUnit?: string,
decimalPrecision: PrecisionOption = PrecisionOptionsEnum.TWO,
): string {
return `${getYAxisFormattedValue(
String(value),
yAxisUnit ?? 'none',
decimalPrecision,
)}`;
}
/** Format column header with optional unit */
export function getTableColumnTitle(title: string, yAxisUnit?: string): string {
if (!yAxisUnit) {
return title;
}
const universalName =
Y_AXIS_UNIT_NAMES[yAxisUnit as keyof typeof Y_AXIS_UNIT_NAMES];
if (!universalName) {
return `${title} (in ${yAxisUnit})`;
}
return `${title} (in ${universalName})`;
}

View File

@@ -96,6 +96,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
config={config}
alignedData={chartData}
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
onCancel={onToggleModelHandler}
/>
);
@@ -105,6 +106,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
chartData,
widget.yAxisUnit,
onToggleModelHandler,
widget.decimalPrecision,
]);
const onPlotDestroy = useCallback(() => {

View File

@@ -95,6 +95,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
config={config}
alignedData={chartData}
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
onCancel={onToggleModelHandler}
/>
);
@@ -104,6 +105,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
chartData,
widget.yAxisUnit,
onToggleModelHandler,
widget.decimalPrecision,
]);
return (

View File

@@ -48,7 +48,9 @@ function ForgotPassword({
}
try {
ErrorResponseHandlerV2(mutationError as AxiosError<ErrorV2Resp>);
ErrorResponseHandlerV2(
(mutationError as unknown) as AxiosError<ErrorV2Resp>,
);
} catch (apiError) {
return apiError as APIError;
}

View File

@@ -178,7 +178,9 @@ export default function HostsListTable({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
tableLayout="fixed"
rowKey={(record): string => record.hostName}
rowKey={(record): string =>
(record as HostRowData & { key: string }).key ?? record.hostName
}
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),

View File

@@ -126,7 +126,8 @@
background: var(--bg-ink-500);
}
.ant-table-cell:has(.hostname-column-value) {
.ant-table-cell:has(.hostname-column-value),
.ant-table-cell:has(.hostname-cell-missing) {
background: var(--bg-ink-400);
}
@@ -139,6 +140,23 @@
letter-spacing: -0.07px;
}
.hostname-cell-missing {
display: flex;
align-items: center;
gap: 4px;
}
.hostname-cell-placeholder {
color: var(--Vanilla-400, #c0c1c3);
}
.hostname-cell-warning-icon {
display: inline-flex;
align-items: center;
margin-left: 4px;
cursor: help;
}
.status-cell {
.active-tag {
color: var(--bg-forest-500);
@@ -357,7 +375,8 @@
color: var(--bg-ink-500);
}
.ant-table-cell:has(.hostname-column-value) {
.ant-table-cell:has(.hostname-column-value),
.ant-table-cell:has(.hostname-cell-missing) {
background: var(--bg-vanilla-100);
}
@@ -365,6 +384,10 @@
color: var(--bg-ink-300);
}
.hostname-cell-placeholder {
color: var(--text-ink-300);
}
.ant-table-tbody > tr:hover > td {
background: rgba(0, 0, 0, 0.04);
}

View File

@@ -1,13 +1,24 @@
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { HostData, TimeSeries } from 'api/infraMonitoring/getHostLists';
import { formatDataForTable, GetHostsQuickFiltersConfig } from '../utils';
import {
formatDataForTable,
GetHostsQuickFiltersConfig,
HostnameCell,
} from '../utils';
const PROGRESS_BAR_CLASS = '.progress-bar';
const emptyTimeSeries: TimeSeries = {
labels: {},
labelsArray: [],
values: [],
};
describe('InfraMonitoringHosts utils', () => {
describe('formatDataForTable', () => {
it('should format host data correctly', () => {
const mockData = [
const mockData: HostData[] = [
{
hostName: 'test-host',
active: true,
@@ -16,8 +27,12 @@ describe('InfraMonitoringHosts utils', () => {
wait: 0.05,
load15: 2.5,
os: 'linux',
cpuTimeSeries: emptyTimeSeries,
memoryTimeSeries: emptyTimeSeries,
waitTimeSeries: emptyTimeSeries,
load15TimeSeries: emptyTimeSeries,
},
] as any;
];
const result = formatDataForTable(mockData);
@@ -46,7 +61,7 @@ describe('InfraMonitoringHosts utils', () => {
});
it('should handle inactive hosts', () => {
const mockData = [
const mockData: HostData[] = [
{
hostName: 'test-host',
active: false,
@@ -55,12 +70,12 @@ describe('InfraMonitoringHosts utils', () => {
wait: 0.02,
load15: 1.2,
os: 'linux',
cpuTimeSeries: [],
memoryTimeSeries: [],
waitTimeSeries: [],
load15TimeSeries: [],
cpuTimeSeries: emptyTimeSeries,
memoryTimeSeries: emptyTimeSeries,
waitTimeSeries: emptyTimeSeries,
load15TimeSeries: emptyTimeSeries,
},
] as any;
];
const result = formatDataForTable(mockData);
@@ -68,6 +83,65 @@ describe('InfraMonitoringHosts utils', () => {
expect(inactiveTag.container.textContent).toBe('INACTIVE');
expect(inactiveTag.container.querySelector('.inactive')).toBeTruthy();
});
it('should set hostName to empty string when host has no hostname', () => {
const mockData: HostData[] = [
{
hostName: '',
active: true,
cpu: 0.5,
memory: 0.4,
wait: 0.01,
load15: 1.0,
os: 'linux',
cpuTimeSeries: emptyTimeSeries,
memoryTimeSeries: emptyTimeSeries,
waitTimeSeries: emptyTimeSeries,
load15TimeSeries: emptyTimeSeries,
},
];
const result = formatDataForTable(mockData);
expect(result[0].hostName).toBe('');
expect(result[0].key).toBe('-0');
});
});
describe('HostnameCell', () => {
it('should render hostname when present (case A: no icon)', () => {
const { container } = render(<HostnameCell hostName="gke-prod-1" />);
expect(container.querySelector('.hostname-column-value')).toBeTruthy();
expect(container.textContent).toBe('gke-prod-1');
expect(container.querySelector('.hostname-cell-missing')).toBeFalsy();
expect(container.querySelector('.hostname-cell-warning-icon')).toBeFalsy();
});
it('should render placeholder and icon when hostName is empty (case B)', () => {
const { container } = render(<HostnameCell hostName="" />);
expect(screen.getByText('-')).toBeTruthy();
expect(container.querySelector('.hostname-cell-missing')).toBeTruthy();
const iconWrapper = container.querySelector('.hostname-cell-warning-icon');
expect(iconWrapper).toBeTruthy();
expect(iconWrapper?.getAttribute('aria-label')).toBe(
'Missing host.name metadata',
);
expect(iconWrapper?.getAttribute('tabindex')).toBe('0');
// Tooltip with "Learn how to configure →" link is shown on hover/focus
});
it('should render placeholder and icon when hostName is whitespace only (case C)', () => {
const { container } = render(<HostnameCell hostName=" " />);
expect(screen.getByText('-')).toBeTruthy();
expect(container.querySelector('.hostname-cell-missing')).toBeTruthy();
expect(container.querySelector('.hostname-cell-warning-icon')).toBeTruthy();
});
it('should render placeholder and icon when hostName is undefined (case D)', () => {
const { container } = render(<HostnameCell hostName={undefined} />);
expect(screen.getByText('-')).toBeTruthy();
expect(container.querySelector('.hostname-cell-missing')).toBeTruthy();
expect(container.querySelector('.hostname-cell-warning-icon')).toBeTruthy();
});
});
describe('GetHostsQuickFiltersConfig', () => {

View File

@@ -1,7 +1,7 @@
import { Dispatch, SetStateAction } from 'react';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Progress, TabsProps, Tag, Tooltip } from 'antd';
import { Progress, TabsProps, Tag, Tooltip, Typography } from 'antd';
import { ColumnType } from 'antd/es/table';
import {
HostData,
@@ -14,6 +14,7 @@ import {
} from 'components/QuickFilters/types';
import TabLabel from 'components/TabLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { TriangleAlert } from 'lucide-react';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
@@ -24,6 +25,7 @@ import HostsList from './HostsList';
import './InfraMonitoring.styles.scss';
export interface HostRowData {
key?: string;
hostName: string;
cpu: React.ReactNode;
memory: React.ReactNode;
@@ -32,6 +34,59 @@ export interface HostRowData {
active: React.ReactNode;
}
const HOSTNAME_DOCS_URL =
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#host-name-is-blankempty';
export function HostnameCell({
hostName,
}: {
hostName?: string | null;
}): React.ReactElement {
const isEmpty = !hostName || !hostName.trim();
if (!isEmpty) {
return <div className="hostname-column-value">{hostName}</div>;
}
return (
<div className="hostname-cell-missing">
<Typography.Text type="secondary" className="hostname-cell-placeholder">
-
</Typography.Text>
<Tooltip
title={
<div>
Missing host.name metadata.
<br />
<a
href={HOSTNAME_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
onClick={(e): void => e.stopPropagation()}
>
Learn how to configure
</a>
</div>
}
trigger={['hover', 'focus']}
>
<span
className="hostname-cell-warning-icon"
tabIndex={0}
role="img"
aria-label="Missing host.name metadata"
onClick={(e): void => e.stopPropagation()}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
e.stopPropagation();
}
}}
>
<TriangleAlert size={14} color={Color.BG_CHERRY_500} />
</span>
</Tooltip>
</div>
);
}
export interface HostsListTableProps {
isLoading: boolean;
isError: boolean;
@@ -75,8 +130,8 @@ export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
dataIndex: 'hostName',
key: 'hostName',
width: 250,
render: (value: string): React.ReactNode => (
<div className="hostname-column-value">{value}</div>
render: (value: string | undefined): React.ReactNode => (
<HostnameCell hostName={value ?? ''} />
),
},
{

View File

@@ -342,7 +342,7 @@ function MultiIngestionSettings(): JSX.Element {
useEffect(() => {
if (isError) {
showErrorNotification(notifications, error as AxiosError);
showErrorNotification(notifications, (error as unknown) as AxiosError);
}
}, [error, isError, notifications]);

View File

@@ -18,8 +18,8 @@ jest.mock('lib/query/createTableColumnsFromQuery', () => ({
jest.mock('container/NewWidget/utils', () => ({
unitOptions: jest.fn(() => [
{ value: 'none', label: 'None' },
{ value: 'percent', label: 'Percent' },
{ value: 'ms', label: 'Milliseconds' },
{ value: '%', label: 'Percent (0 - 100)' },
{ value: 'ms', label: 'Milliseconds (ms)' },
]),
}));
@@ -39,7 +39,7 @@ const defaultProps = {
],
thresholdTableOptions: 'cpu_usage',
columnUnits: { cpu_usage: 'percent', memory_usage: 'bytes' },
yAxisUnit: 'percent',
yAxisUnit: '%',
moveThreshold: jest.fn(),
};

View File

@@ -1,99 +0,0 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { AutoComplete, Input, Typography } from 'antd';
import { find } from 'lodash-es';
import { flattenedCategories } from './dataFormatCategories';
const findCategoryById = (
searchValue: string,
): Record<string, string> | undefined =>
find(flattenedCategories, (option) => option.id === searchValue);
const findCategoryByName = (
searchValue: string,
): Record<string, string> | undefined =>
find(flattenedCategories, (option) => option.name === searchValue);
type OnSelectType = Dispatch<SetStateAction<string>> | ((val: string) => void);
/**
* @deprecated Use DashboardYAxisUnitSelectorWrapper instead.
*/
function YAxisUnitSelector({
value,
onSelect,
fieldLabel,
handleClear,
}: {
value: string;
onSelect: OnSelectType;
fieldLabel: string;
handleClear?: () => void;
}): JSX.Element {
const [inputValue, setInputValue] = useState('');
// Sync input value with the actual value prop
useEffect(() => {
const category = findCategoryById(value);
setInputValue(category?.name || '');
}, [value]);
const onSelectHandler = (selectedValue: string): void => {
const category = findCategoryByName(selectedValue);
if (category) {
onSelect(category.id);
setInputValue(selectedValue);
}
};
const onChangeHandler = (inputValue: string): void => {
setInputValue(inputValue);
// Clear the yAxisUnit if input is empty or doesn't match any option
if (!inputValue) {
onSelect('');
}
};
const onClearHandler = (): void => {
setInputValue('');
onSelect('');
if (handleClear) {
handleClear();
}
};
const options = flattenedCategories.map((options) => ({
value: options.name,
}));
return (
<div className="y-axis-unit-selector">
<Typography.Text className="heading">{fieldLabel}</Typography.Text>
<AutoComplete
style={{ width: '100%' }}
rootClassName="y-axis-root-popover"
options={options}
allowClear
value={inputValue}
onChange={onChangeHandler}
onClear={onClearHandler}
onSelect={onSelectHandler}
filterOption={(inputValue, option): boolean => {
if (option) {
return (
option.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
);
}
return false;
}}
>
<Input placeholder="Unit" rootClassName="input" />
</AutoComplete>
</div>
);
}
export default YAxisUnitSelector;
YAxisUnitSelector.defaultProps = {
handleClear: (): void => {},
};

View File

@@ -1,240 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { act } from 'react-dom/test-utils';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import YAxisUnitSelector from '../YAxisUnitSelector';
// Mock the dataFormatCategories to have predictable test data
jest.mock('../dataFormatCategories', () => ({
flattenedCategories: [
{ id: 'seconds', name: 'seconds (s)' },
{ id: 'milliseconds', name: 'milliseconds (ms)' },
{ id: 'hours', name: 'hours (h)' },
{ id: 'minutes', name: 'minutes (m)' },
],
}));
const MOCK_SECONDS = 'seconds';
const MOCK_MILLISECONDS = 'milliseconds';
describe('YAxisUnitSelector', () => {
const defaultProps = {
value: MOCK_SECONDS,
onSelect: jest.fn(),
fieldLabel: 'Y Axis Unit',
handleClear: jest.fn(),
};
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
jest.clearAllMocks();
user = userEvent.setup();
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('Rendering (Read) & (write)', () => {
it('renders with correct field label', () => {
render(
<YAxisUnitSelector
value={defaultProps.value}
onSelect={defaultProps.onSelect}
fieldLabel={defaultProps.fieldLabel}
handleClear={defaultProps.handleClear}
/>,
);
expect(screen.getByText('Y Axis Unit')).toBeInTheDocument();
const input = screen.getByRole('combobox');
expect(input).toHaveValue('seconds (s)');
});
it('renders with custom field label', () => {
render(
<YAxisUnitSelector
value={defaultProps.value}
onSelect={defaultProps.onSelect}
fieldLabel="Custom Unit Label"
handleClear={defaultProps.handleClear}
/>,
);
expect(screen.getByText('Custom Unit Label')).toBeInTheDocument();
});
it('displays empty input when value prop is empty', () => {
render(
<YAxisUnitSelector
value=""
onSelect={defaultProps.onSelect}
fieldLabel={defaultProps.fieldLabel}
handleClear={defaultProps.handleClear}
/>,
);
expect(screen.getByDisplayValue('')).toBeInTheDocument();
});
it('shows placeholder text', () => {
render(
<YAxisUnitSelector
value={defaultProps.value}
onSelect={defaultProps.onSelect}
fieldLabel={defaultProps.fieldLabel}
handleClear={defaultProps.handleClear}
/>,
);
expect(screen.getByPlaceholderText('Unit')).toBeInTheDocument();
});
it('handles numeric input', async () => {
render(
<YAxisUnitSelector
value={defaultProps.value}
onSelect={defaultProps.onSelect}
fieldLabel={defaultProps.fieldLabel}
handleClear={defaultProps.handleClear}
/>,
);
const input = screen.getByRole('combobox');
await user.clear(input);
await user.type(input, '12345');
expect(input).toHaveValue('12345');
});
it('handles mixed content input', async () => {
render(
<YAxisUnitSelector
value={defaultProps.value}
onSelect={defaultProps.onSelect}
fieldLabel={defaultProps.fieldLabel}
handleClear={defaultProps.handleClear}
/>,
);
const input = screen.getByRole('combobox');
await user.clear(input);
await user.type(input, 'Test123!@#');
expect(input).toHaveValue('Test123!@#');
});
});
describe('State Management', () => {
it('syncs input value with value prop changes', async () => {
const { rerender } = render(
<YAxisUnitSelector
value={defaultProps.value}
onSelect={defaultProps.onSelect}
fieldLabel={defaultProps.fieldLabel}
handleClear={defaultProps.handleClear}
/>,
);
const input = screen.getByRole('combobox');
// Initial value
expect(input).toHaveValue('seconds (s)');
// Change value prop
rerender(
<YAxisUnitSelector
value={MOCK_MILLISECONDS}
onSelect={defaultProps.onSelect}
fieldLabel={defaultProps.fieldLabel}
handleClear={defaultProps.handleClear}
/>,
);
await waitFor(() => {
expect(input).toHaveValue('milliseconds (ms)');
});
});
it('handles empty value prop correctly', async () => {
const { rerender } = render(
<YAxisUnitSelector
value={defaultProps.value}
onSelect={defaultProps.onSelect}
fieldLabel={defaultProps.fieldLabel}
handleClear={defaultProps.handleClear}
/>,
);
const input = screen.getByRole('combobox');
// Change to empty value
rerender(
<YAxisUnitSelector
value=""
onSelect={defaultProps.onSelect}
fieldLabel={defaultProps.fieldLabel}
handleClear={defaultProps.handleClear}
/>,
);
await waitFor(() => {
expect(input).toHaveValue('');
});
});
it('handles invalid value prop gracefully', async () => {
const { rerender } = render(
<YAxisUnitSelector
value={defaultProps.value}
onSelect={defaultProps.onSelect}
fieldLabel={defaultProps.fieldLabel}
handleClear={defaultProps.handleClear}
/>,
);
const input = screen.getByRole('combobox');
// Change to invalid value
rerender(
<YAxisUnitSelector
value="invalid_id"
onSelect={defaultProps.onSelect}
fieldLabel={defaultProps.fieldLabel}
handleClear={defaultProps.handleClear}
/>,
);
await waitFor(() => {
expect(input).toHaveValue('');
});
});
it('maintains local state during typing', async () => {
render(
<YAxisUnitSelector
value={defaultProps.value}
onSelect={defaultProps.onSelect}
fieldLabel={defaultProps.fieldLabel}
handleClear={defaultProps.handleClear}
/>,
);
const input = screen.getByRole('combobox');
// first clear then type
await user.clear(input);
await user.type(input, 'test');
expect(input).toHaveValue('test');
// Value prop change should not override local typing
await act(async () => {
// Simulate prop change
render(
<YAxisUnitSelector
value="bytes"
onSelect={defaultProps.onSelect}
fieldLabel={defaultProps.fieldLabel}
handleClear={defaultProps.handleClear}
/>,
);
});
// Local typing should be preserved
expect(input).toHaveValue('test');
});
});
});

View File

@@ -1,613 +1,53 @@
import { flattenDeep } from 'lodash-es';
import {
AccelerationFormats,
AngularFormats,
AreaFormats,
BooleanFormats,
CategoryNames,
ConcentrationFormats,
CurrencyFormats,
DataFormats,
DataRateFormats,
DataTypeCategories,
DatetimeFormats,
FlopsFormats,
FlowFormats,
ForceFormats,
HashRateFormats,
LengthFormats,
MassFormats,
MiscellaneousFormats,
PowerElectricalFormats,
PressureFormats,
RadiationFormats,
RotationSpeedFormats,
TemperatureFormats,
ThroughputFormats,
TimeFormats,
VelocityFormats,
VolumeFormats,
} from './types';
UniversalUnitToGrafanaUnit,
YAxisCategoryNames,
} from 'components/YAxisUnitSelector/constants';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';
import { convertValue } from 'lib/getConvertedValue';
export const dataTypeCategories: DataTypeCategories = [
{
name: CategoryNames.Time,
formats: [
{ name: 'Hertz (1/s)', id: TimeFormats.Hertz },
{ name: 'nanoseconds (ns)', id: TimeFormats.Nanoseconds },
{ name: 'microseconds (µs)', id: TimeFormats.Microseconds },
{ name: 'milliseconds (ms)', id: TimeFormats.Milliseconds },
{ name: 'seconds (s)', id: TimeFormats.Seconds },
{ name: 'minutes (m)', id: TimeFormats.Minutes },
{ name: 'hours (h)', id: TimeFormats.Hours },
{ name: 'days (d)', id: TimeFormats.Days },
{ name: 'duration in ms (dtdurationms)', id: TimeFormats.DurationMs },
{ name: 'duration in s (dtdurations)', id: TimeFormats.DurationS },
{ name: 'duration in h:m:s (dthms)', id: TimeFormats.DurationHms },
{ name: 'duration in d:h:m:s (dtdhms)', id: TimeFormats.DurationDhms },
{ name: 'timeticks (timeticks)', id: TimeFormats.Timeticks },
{ name: 'clock in ms (clockms)', id: TimeFormats.ClockMs },
{ name: 'clock in s (clocks)', id: TimeFormats.ClockS },
],
},
{
name: CategoryNames.Throughput,
formats: [
{ name: 'counts/sec (cps)', id: ThroughputFormats.CountsPerSec },
{ name: 'ops/sec (ops)', id: ThroughputFormats.OpsPerSec },
{ name: 'requests/sec (reqps)', id: ThroughputFormats.RequestsPerSec },
{ name: 'reads/sec (rps)', id: ThroughputFormats.ReadsPerSec },
{ name: 'writes/sec (wps)', id: ThroughputFormats.WritesPerSec },
{ name: 'I/O operations/sec (iops)', id: ThroughputFormats.IOOpsPerSec },
{ name: 'counts/min (cpm)', id: ThroughputFormats.CountsPerMin },
{ name: 'ops/min (opm)', id: ThroughputFormats.OpsPerMin },
{ name: 'reads/min (rpm)', id: ThroughputFormats.ReadsPerMin },
{ name: 'writes/min (wpm)', id: ThroughputFormats.WritesPerMin },
],
},
{
name: CategoryNames.Data,
formats: [
{ name: 'bytes(IEC)', id: DataFormats.BytesIEC },
{ name: 'bytes(SI)', id: DataFormats.BytesSI },
{ name: 'bits(IEC)', id: DataFormats.BitsIEC },
{ name: 'bits(SI)', id: DataFormats.BitsSI },
{ name: 'kibibytes', id: DataFormats.KibiBytes },
{ name: 'kilobytes', id: DataFormats.KiloBytes },
{ name: 'mebibytes', id: DataFormats.MebiBytes },
{ name: 'megabytes', id: DataFormats.MegaBytes },
{ name: 'gibibytes', id: DataFormats.GibiBytes },
{ name: 'gigabytes', id: DataFormats.GigaBytes },
{ name: 'tebibytes', id: DataFormats.TebiBytes },
{ name: 'terabytes', id: DataFormats.TeraBytes },
{ name: 'pebibytes', id: DataFormats.PebiBytes },
{ name: 'petabytes', id: DataFormats.PetaBytes },
],
},
{
name: CategoryNames.DataRate,
formats: [
{ name: 'packets/sec', id: DataRateFormats.PacketsPerSec },
{ name: 'bytes/sec(IEC)', id: DataRateFormats.BytesPerSecIEC },
{ name: 'bytes/sec(SI)', id: DataRateFormats.BytesPerSecSI },
{ name: 'bits/sec(IEC)', id: DataRateFormats.BitsPerSecIEC },
{ name: 'bits/sec(SI)', id: DataRateFormats.BitsPerSecSI },
{ name: 'kibibytes/sec', id: DataRateFormats.KibiBytesPerSec },
{ name: 'kibibits/sec', id: DataRateFormats.KibiBitsPerSec },
{ name: 'kilobytes/sec', id: DataRateFormats.KiloBytesPerSec },
{ name: 'kilobits/sec', id: DataRateFormats.KiloBitsPerSec },
{ name: 'mebibytes/sec', id: DataRateFormats.MebiBytesPerSec },
{ name: 'mebibits/sec', id: DataRateFormats.MebiBitsPerSec },
{ name: 'megabytes/sec', id: DataRateFormats.MegaBytesPerSec },
{ name: 'megabits/sec', id: DataRateFormats.MegaBitsPerSec },
{ name: 'gibibytes/sec', id: DataRateFormats.GibiBytesPerSec },
{ name: 'gibibits/sec', id: DataRateFormats.GibiBitsPerSec },
{ name: 'gigabytes/sec', id: DataRateFormats.GigaBytesPerSec },
{ name: 'gigabits/sec', id: DataRateFormats.GigaBitsPerSec },
{ name: 'tebibytes/sec', id: DataRateFormats.TebiBytesPerSec },
{ name: 'tebibits/sec', id: DataRateFormats.TebiBitsPerSec },
{ name: 'terabytes/sec', id: DataRateFormats.TeraBytesPerSec },
{ name: 'terabits/sec', id: DataRateFormats.TeraBitsPerSec },
{ name: 'pebibytes/sec', id: DataRateFormats.PebiBytesPerSec },
{ name: 'pebibits/sec', id: DataRateFormats.PebiBitsPerSec },
{ name: 'petabytes/sec', id: DataRateFormats.PetaBytesPerSec },
{ name: 'petabits/sec', id: DataRateFormats.PetaBitsPerSec },
],
},
{
name: CategoryNames.HashRate,
formats: [
{ name: 'hashes/sec', id: HashRateFormats.HashesPerSec },
{ name: 'kilohashes/sec', id: HashRateFormats.KiloHashesPerSec },
{ name: 'megahashes/sec', id: HashRateFormats.MegaHashesPerSec },
{ name: 'gigahashes/sec', id: HashRateFormats.GigaHashesPerSec },
{ name: 'terahashes/sec', id: HashRateFormats.TeraHashesPerSec },
{ name: 'petahashes/sec', id: HashRateFormats.PetaHashesPerSec },
{ name: 'exahashes/sec', id: HashRateFormats.ExaHashesPerSec },
],
},
{
name: CategoryNames.Miscellaneous,
formats: [
{ name: 'none', id: MiscellaneousFormats.None },
{ name: 'String', id: MiscellaneousFormats.String },
{ name: 'short', id: MiscellaneousFormats.Short },
{ name: 'Percent (0-100)', id: MiscellaneousFormats.Percent },
{ name: 'Percent (0.0-1.0)', id: MiscellaneousFormats.PercentUnit },
{ name: 'Humidity (%H)', id: MiscellaneousFormats.Humidity },
{ name: 'Decibel', id: MiscellaneousFormats.Decibel },
{ name: 'Hexadecimal (0x)', id: MiscellaneousFormats.Hexadecimal0x },
{ name: 'Hexadecimal', id: MiscellaneousFormats.Hexadecimal },
{ name: 'Scientific notation', id: MiscellaneousFormats.ScientificNotation },
{ name: 'Locale format', id: MiscellaneousFormats.LocaleFormat },
{ name: 'Pixels', id: MiscellaneousFormats.Pixels },
],
},
{
name: CategoryNames.Acceleration,
formats: [
{ name: 'Meters/sec²', id: AccelerationFormats.MetersPerSecondSquared },
{ name: 'Feet/sec²', id: AccelerationFormats.FeetPerSecondSquared },
{ name: 'G unit', id: AccelerationFormats.GUnit },
],
},
{
name: CategoryNames.Angle,
formats: [
{ name: 'Degrees (°)', id: AngularFormats.Degree },
{ name: 'Radians', id: AngularFormats.Radian },
{ name: 'Gradian', id: AngularFormats.Gradian },
{ name: 'Arc Minutes', id: AngularFormats.ArcMinute },
{ name: 'Arc Seconds', id: AngularFormats.ArcSecond },
],
},
{
name: CategoryNames.Area,
formats: [
{ name: 'Square Meters (m²)', id: AreaFormats.SquareMeters },
{ name: 'Square Feet (ft²)', id: AreaFormats.SquareFeet },
{ name: 'Square Miles (mi²)', id: AreaFormats.SquareMiles },
],
},
{
name: CategoryNames.Computation,
formats: [
{ name: 'FLOP/s', id: FlopsFormats.FLOPs },
{ name: 'MFLOP/s', id: FlopsFormats.MFLOPs },
{ name: 'GFLOP/s', id: FlopsFormats.GFLOPs },
{ name: 'TFLOP/s', id: FlopsFormats.TFLOPs },
{ name: 'PFLOP/s', id: FlopsFormats.PFLOPs },
{ name: 'EFLOP/s', id: FlopsFormats.EFLOPs },
{ name: 'ZFLOP/s', id: FlopsFormats.ZFLOPs },
{ name: 'YFLOP/s', id: FlopsFormats.YFLOPs },
],
},
{
name: CategoryNames.Concentration,
formats: [
{ name: 'parts-per-million (ppm)', id: ConcentrationFormats.PPM },
{ name: 'parts-per-billion (ppb)', id: ConcentrationFormats.PPB },
{ name: 'nanogram per cubic meter (ng/m³)', id: ConcentrationFormats.NgM3 },
{
name: 'nanogram per normal cubic meter (ng/Nm³)',
id: ConcentrationFormats.NgNM3,
},
{ name: 'microgram per cubic meter (μg/m³)', id: ConcentrationFormats.UgM3 },
{
name: 'microgram per normal cubic meter (μg/Nm³)',
id: ConcentrationFormats.UgNM3,
},
{ name: 'milligram per cubic meter (mg/m³)', id: ConcentrationFormats.MgM3 },
{
name: 'milligram per normal cubic meter (mg/Nm³)',
id: ConcentrationFormats.MgNM3,
},
{ name: 'gram per cubic meter (g/m³)', id: ConcentrationFormats.GM3 },
{
name: 'gram per normal cubic meter (g/Nm³)',
id: ConcentrationFormats.GNM3,
},
{ name: 'milligrams per decilitre (mg/dL)', id: ConcentrationFormats.MgDL },
{ name: 'millimoles per litre (mmol/L)', id: ConcentrationFormats.MmolL },
],
},
{
name: CategoryNames.Currency,
formats: [
{ name: 'Dollars ($)', id: CurrencyFormats.USD },
{ name: 'Pounds (£)', id: CurrencyFormats.GBP },
{ name: 'Euro (€)', id: CurrencyFormats.EUR },
{ name: 'Yen (¥)', id: CurrencyFormats.JPY },
{ name: 'Rubles (₽)', id: CurrencyFormats.RUB },
{ name: 'Hryvnias (₴)', id: CurrencyFormats.UAH },
{ name: 'Real (R$)', id: CurrencyFormats.BRL },
{ name: 'Danish Krone (kr)', id: CurrencyFormats.DKK },
{ name: 'Icelandic Króna (kr)', id: CurrencyFormats.ISK },
{ name: 'Norwegian Krone (kr)', id: CurrencyFormats.NOK },
{ name: 'Swedish Krona (kr)', id: CurrencyFormats.SEK },
{ name: 'Czech koruna (czk)', id: CurrencyFormats.CZK },
{ name: 'Swiss franc (CHF)', id: CurrencyFormats.CHF },
{ name: 'Polish Złoty (PLN)', id: CurrencyFormats.PLN },
{ name: 'Bitcoin (฿)', id: CurrencyFormats.BTC },
{ name: 'Milli Bitcoin (฿)', id: CurrencyFormats.MBTC },
{ name: 'Micro Bitcoin (฿)', id: CurrencyFormats.UBTC },
{ name: 'South African Rand (R)', id: CurrencyFormats.ZAR },
{ name: 'Indian Rupee (₹)', id: CurrencyFormats.INR },
{ name: 'South Korean Won (₩)', id: CurrencyFormats.KRW },
{ name: 'Indonesian Rupiah (Rp)', id: CurrencyFormats.IDR },
{ name: 'Philippine Peso (PHP)', id: CurrencyFormats.PHP },
{ name: 'Vietnamese Dong (VND)', id: CurrencyFormats.VND },
],
},
{
name: CategoryNames.Datetime,
formats: [
{ name: 'Datetime ISO', id: DatetimeFormats.ISO },
{
name: 'Datetime ISO (No date if today)',
id: DatetimeFormats.ISONoDateIfToday,
},
{ name: 'Datetime US', id: DatetimeFormats.US },
{
name: 'Datetime US (No date if today)',
id: DatetimeFormats.USNoDateIfToday,
},
{ name: 'Datetime local', id: DatetimeFormats.Local },
{
name: 'Datetime local (No date if today)',
id: DatetimeFormats.LocalNoDateIfToday,
},
{ name: 'Datetime default', id: DatetimeFormats.System },
{ name: 'From Now', id: DatetimeFormats.FromNow },
],
},
{
name: CategoryNames.Energy,
formats: [
{ name: 'Watt (W)', id: PowerElectricalFormats.WATT },
{ name: 'Kilowatt (kW)', id: PowerElectricalFormats.KWATT },
{ name: 'Megawatt (MW)', id: PowerElectricalFormats.MEGWATT },
{ name: 'Gigawatt (GW)', id: PowerElectricalFormats.GWATT },
{ name: 'Milliwatt (mW)', id: PowerElectricalFormats.MWATT },
{ name: 'Watt per square meter (W/m²)', id: PowerElectricalFormats.WM2 },
{ name: 'Volt-Ampere (VA)', id: PowerElectricalFormats.VOLTAMP },
{ name: 'Kilovolt-Ampere (kVA)', id: PowerElectricalFormats.KVOLTAMP },
{
name: 'Volt-Ampere reactive (VAr)',
id: PowerElectricalFormats.VOLTAMPREACT,
},
{
name: 'Kilovolt-Ampere reactive (kVAr)',
id: PowerElectricalFormats.KVOLTAMPREACT,
},
{ name: 'Watt-hour (Wh)', id: PowerElectricalFormats.WATTH },
{
name: 'Watt-hour per Kilogram (Wh/kg)',
id: PowerElectricalFormats.WATTHPERKG,
},
{ name: 'Kilowatt-hour (kWh)', id: PowerElectricalFormats.KWATTH },
{ name: 'Kilowatt-min (kWm)', id: PowerElectricalFormats.KWATTM },
{ name: 'Ampere-hour (Ah)', id: PowerElectricalFormats.AMPH },
{ name: 'Kiloampere-hour (kAh)', id: PowerElectricalFormats.KAMPH },
{ name: 'Milliampere-hour (mAh)', id: PowerElectricalFormats.MAMPH },
{ name: 'Joule (J)', id: PowerElectricalFormats.JOULE },
{ name: 'Electron volt (eV)', id: PowerElectricalFormats.EV },
{ name: 'Ampere (A)', id: PowerElectricalFormats.AMP },
{ name: 'Kiloampere (kA)', id: PowerElectricalFormats.KAMP },
{ name: 'Milliampere (mA)', id: PowerElectricalFormats.MAMP },
{ name: 'Volt (V)', id: PowerElectricalFormats.VOLT },
{ name: 'Kilovolt (kV)', id: PowerElectricalFormats.KVOLT },
{ name: 'Millivolt (mV)', id: PowerElectricalFormats.MVOLT },
{ name: 'Decibel-milliwatt (dBm)', id: PowerElectricalFormats.DBM },
{ name: 'Ohm (Ω)', id: PowerElectricalFormats.OHM },
{ name: 'Kiloohm (kΩ)', id: PowerElectricalFormats.KOHM },
{ name: 'Megaohm (MΩ)', id: PowerElectricalFormats.MOHM },
{ name: 'Farad (F)', id: PowerElectricalFormats.FARAD },
{ name: 'Microfarad (µF)', id: PowerElectricalFormats.µFARAD },
{ name: 'Nanofarad (nF)', id: PowerElectricalFormats.NFARAD },
{ name: 'Picofarad (pF)', id: PowerElectricalFormats.PFARAD },
{ name: 'Femtofarad (fF)', id: PowerElectricalFormats.FFARAD },
{ name: 'Henry (H)', id: PowerElectricalFormats.HENRY },
{ name: 'Millihenry (mH)', id: PowerElectricalFormats.MHENRY },
{ name: 'Microhenry (µH)', id: PowerElectricalFormats.µHENRY },
{ name: 'Lumens (Lm)', id: PowerElectricalFormats.LUMENS },
],
},
{
name: CategoryNames.Flow,
formats: [
{ name: 'Gallons/min (gpm)', id: FlowFormats.FLOWGPM },
{ name: 'Cubic meters/sec (cms)', id: FlowFormats.FLOWCMS },
{ name: 'Cubic feet/sec (cfs)', id: FlowFormats.FLOWCFS },
{ name: 'Cubic feet/min (cfm)', id: FlowFormats.FLOWCFM },
{ name: 'Litre/hour', id: FlowFormats.LITREH },
{ name: 'Litre/min (L/min)', id: FlowFormats.FLOWLPM },
{ name: 'milliLitre/min (mL/min)', id: FlowFormats.FLOWMLPM },
{ name: 'Lux (lx)', id: FlowFormats.LUX },
],
},
{
name: CategoryNames.Force,
formats: [
{ name: 'Newton-meters (Nm)', id: ForceFormats.FORCENM },
{ name: 'Kilonewton-meters (kNm)', id: ForceFormats.FORCEKNM },
{ name: 'Newtons (N)', id: ForceFormats.FORCEN },
{ name: 'Kilonewtons (kN)', id: ForceFormats.FORCEKN },
],
},
{
name: CategoryNames.Mass,
formats: [
{ name: 'milligram (mg)', id: MassFormats.MASSMG },
{ name: 'gram (g)', id: MassFormats.MASSG },
{ name: 'pound (lb)', id: MassFormats.MASSLB },
{ name: 'kilogram (kg)', id: MassFormats.MASSKG },
{ name: 'metric ton (t)', id: MassFormats.MASST },
],
},
{
name: CategoryNames.Length,
formats: [
{ name: 'millimeter (mm)', id: LengthFormats.LENGTHMM },
{ name: 'inch (in)', id: LengthFormats.LENGTHIN },
{ name: 'feet (ft)', id: LengthFormats.LENGTHFT },
{ name: 'meter (m)', id: LengthFormats.LENGTHM },
{ name: 'kilometer (km)', id: LengthFormats.LENGTHKM },
{ name: 'mile (mi)', id: LengthFormats.LENGTHMI },
],
},
{
name: CategoryNames.Pressure,
formats: [
{ name: 'Millibars', id: PressureFormats.PRESSUREMBAR },
{ name: 'Bars', id: PressureFormats.PRESSUREBAR },
{ name: 'Kilobars', id: PressureFormats.PRESSUREKBAR },
{ name: 'Pascals', id: PressureFormats.PRESSUREPA },
{ name: 'Hectopascals', id: PressureFormats.PRESSUREHPA },
{ name: 'Kilopascals', id: PressureFormats.PRESSUREKPA },
{ name: 'Inches of mercury', id: PressureFormats.PRESSUREHG },
{ name: 'PSI', id: PressureFormats.PRESSUREPSI },
],
},
{
name: CategoryNames.Radiation,
formats: [
{ name: 'Becquerel (Bq)', id: RadiationFormats.RADBQ },
{ name: 'curie (Ci)', id: RadiationFormats.RADCI },
{ name: 'Gray (Gy)', id: RadiationFormats.RADGY },
{ name: 'rad', id: RadiationFormats.RADRAD },
{ name: 'Sievert (Sv)', id: RadiationFormats.RADSV },
{ name: 'milliSievert (mSv)', id: RadiationFormats.RADMSV },
{ name: 'microSievert (µSv)', id: RadiationFormats.RADUSV },
{ name: 'rem', id: RadiationFormats.RADREM },
{ name: 'Exposure (C/kg)', id: RadiationFormats.RADEXPCKG },
{ name: 'roentgen (R)', id: RadiationFormats.RADR },
{ name: 'Sievert/hour (Sv/h)', id: RadiationFormats.RADSVH },
{ name: 'milliSievert/hour (mSv/h)', id: RadiationFormats.RADMSVH },
{ name: 'microSievert/hour (µSv/h)', id: RadiationFormats.RADUSVH },
],
},
{
name: CategoryNames.RotationSpeed,
formats: [
{ name: 'Revolutions per minute (rpm)', id: RotationSpeedFormats.ROTRPM },
{ name: 'Hertz (Hz)', id: RotationSpeedFormats.ROTHZ },
{ name: 'Radians per second (rad/s)', id: RotationSpeedFormats.ROTRADS },
{ name: 'Degrees per second (°/s)', id: RotationSpeedFormats.ROTDEGS },
],
},
{
name: CategoryNames.Temperature,
formats: [
{ name: 'Celsius (°C)', id: TemperatureFormats.CELSIUS },
{ name: 'Fahrenheit (°F)', id: TemperatureFormats.FAHRENHEIT },
{ name: 'Kelvin (K)', id: TemperatureFormats.KELVIN },
],
},
{
name: CategoryNames.Velocity,
formats: [
{ name: 'meters/second (m/s)', id: VelocityFormats.METERS_PER_SECOND },
{ name: 'kilometers/hour (km/h)', id: VelocityFormats.KILOMETERS_PER_HOUR },
{ name: 'miles/hour (mph)', id: VelocityFormats.MILES_PER_HOUR },
{ name: 'knot (kn)', id: VelocityFormats.KNOT },
],
},
{
name: CategoryNames.Volume,
formats: [
{ name: 'millilitre (mL)', id: VolumeFormats.MILLILITRE },
{ name: 'litre (L)', id: VolumeFormats.LITRE },
{ name: 'cubic meter', id: VolumeFormats.CUBIC_METER },
{ name: 'Normal cubic meter', id: VolumeFormats.NORMAL_CUBIC_METER },
{ name: 'cubic decimeter', id: VolumeFormats.CUBIC_DECIMETER },
{ name: 'gallons', id: VolumeFormats.GALLONS },
],
},
{
name: CategoryNames.Boolean,
formats: [
{ name: 'True / False', id: BooleanFormats.TRUE_FALSE },
{ name: 'Yes / No', id: BooleanFormats.YES_NO },
{ name: 'On / Off', id: BooleanFormats.ON_OFF },
],
},
];
// Function to get the category name for a given unit ID (Grafana or universal)
export const getCategoryName = (unitId: string): YAxisCategoryNames | null => {
const categories = getYAxisCategories(YAxisSource.DASHBOARDS);
export const flattenedCategories = flattenDeep(
dataTypeCategories.map((category) => category.formats),
);
const foundCategory = categories.find((category) =>
category.units.some((unit) => {
// Units in Y-axis categories use universal unit IDs.
// Thresholds / column units often use Grafana-style IDs.
// Treat a unit as matching if either:
// - it is already the universal ID, or
// - it matches the mapped Grafana ID for that universal unit.
if (unit.id === unitId) {
return true;
}
type ConversionFactors = {
[key: string]: {
[key: string]: number | null;
};
const grafanaId = UniversalUnitToGrafanaUnit[unit.id];
return grafanaId === unitId;
}),
);
return foundCategory ? foundCategory.name : null;
};
// Object containing conversion factors for various categories and formats
const conversionFactors: ConversionFactors = {
[CategoryNames.Time]: {
[TimeFormats.Hertz]: 1,
[TimeFormats.Nanoseconds]: 1e-9,
[TimeFormats.Microseconds]: 1e-6,
[TimeFormats.Milliseconds]: 1e-3,
[TimeFormats.Seconds]: 1,
[TimeFormats.Minutes]: 60,
[TimeFormats.Hours]: 3600,
[TimeFormats.Days]: 86400,
[TimeFormats.DurationMs]: 1e-3,
[TimeFormats.DurationS]: 1,
[TimeFormats.DurationHms]: null, // Requires special handling
[TimeFormats.DurationDhms]: null, // Requires special handling
[TimeFormats.Timeticks]: null, // Requires special handling
[TimeFormats.ClockMs]: 1e-3,
[TimeFormats.ClockS]: 1,
},
[CategoryNames.Throughput]: {
[ThroughputFormats.CountsPerSec]: 1,
[ThroughputFormats.OpsPerSec]: 1,
[ThroughputFormats.RequestsPerSec]: 1,
[ThroughputFormats.ReadsPerSec]: 1,
[ThroughputFormats.WritesPerSec]: 1,
[ThroughputFormats.IOOpsPerSec]: 1,
[ThroughputFormats.CountsPerMin]: 1 / 60,
[ThroughputFormats.OpsPerMin]: 1 / 60,
[ThroughputFormats.ReadsPerMin]: 1 / 60,
[ThroughputFormats.WritesPerMin]: 1 / 60,
},
[CategoryNames.Data]: {
[DataFormats.BytesIEC]: 1,
[DataFormats.BytesSI]: 1,
[DataFormats.BitsIEC]: 0.125,
[DataFormats.BitsSI]: 0.125,
[DataFormats.KibiBytes]: 1024,
[DataFormats.KiloBytes]: 1000,
[DataFormats.MebiBytes]: 1048576,
[DataFormats.MegaBytes]: 1000000,
[DataFormats.GibiBytes]: 1073741824,
[DataFormats.GigaBytes]: 1000000000,
[DataFormats.TebiBytes]: 1099511627776,
[DataFormats.TeraBytes]: 1000000000000,
[DataFormats.PebiBytes]: 1125899906842624,
[DataFormats.PetaBytes]: 1000000000000000,
},
[CategoryNames.DataRate]: {
[DataRateFormats.PacketsPerSec]: null, // Cannot convert directly to other data rates
[DataRateFormats.BytesPerSecIEC]: 1,
[DataRateFormats.BytesPerSecSI]: 1,
[DataRateFormats.BitsPerSecIEC]: 0.125,
[DataRateFormats.BitsPerSecSI]: 0.125,
[DataRateFormats.KibiBytesPerSec]: 1024,
[DataRateFormats.KibiBitsPerSec]: 128,
[DataRateFormats.KiloBytesPerSec]: 1000,
[DataRateFormats.KiloBitsPerSec]: 125,
[DataRateFormats.MebiBytesPerSec]: 1048576,
[DataRateFormats.MebiBitsPerSec]: 131072,
[DataRateFormats.MegaBytesPerSec]: 1000000,
[DataRateFormats.MegaBitsPerSec]: 125000,
[DataRateFormats.GibiBytesPerSec]: 1073741824,
[DataRateFormats.GibiBitsPerSec]: 134217728,
[DataRateFormats.GigaBytesPerSec]: 1000000000,
[DataRateFormats.GigaBitsPerSec]: 125000000,
[DataRateFormats.TebiBytesPerSec]: 1099511627776,
[DataRateFormats.TebiBitsPerSec]: 137438953472,
[DataRateFormats.TeraBytesPerSec]: 1000000000000,
[DataRateFormats.TeraBitsPerSec]: 125000000000,
[DataRateFormats.PebiBytesPerSec]: 1125899906842624,
[DataRateFormats.PebiBitsPerSec]: 140737488355328,
[DataRateFormats.PetaBytesPerSec]: 1000000000000000,
[DataRateFormats.PetaBitsPerSec]: 125000000000000,
},
[CategoryNames.Miscellaneous]: {
[MiscellaneousFormats.None]: null,
[MiscellaneousFormats.String]: null,
[MiscellaneousFormats.Short]: null,
[MiscellaneousFormats.Percent]: 1,
[MiscellaneousFormats.PercentUnit]: 100,
[MiscellaneousFormats.Humidity]: 1,
[MiscellaneousFormats.Decibel]: null,
[MiscellaneousFormats.Hexadecimal0x]: null,
[MiscellaneousFormats.Hexadecimal]: null,
[MiscellaneousFormats.ScientificNotation]: null,
[MiscellaneousFormats.LocaleFormat]: null,
[MiscellaneousFormats.Pixels]: null,
},
[CategoryNames.Boolean]: {
[BooleanFormats.TRUE_FALSE]: null, // Not convertible
[BooleanFormats.YES_NO]: null, // Not convertible
[BooleanFormats.ON_OFF]: null, // Not convertible
},
};
// Function to get the conversion factor between two units in a specific category
function getConversionFactor(
fromUnit: string,
toUnit: string,
category: CategoryNames,
): number | null {
// Retrieves the conversion factors for the specified category
const categoryFactors = conversionFactors[category];
if (!categoryFactors) {
return null; // Returns null if the category does not exist
}
const fromFactor = categoryFactors[fromUnit];
const toFactor = categoryFactors[toUnit];
if (
fromFactor === undefined ||
toFactor === undefined ||
fromFactor === null ||
toFactor === null
) {
return null; // Returns null if either unit does not exist or is not convertible
}
return fromFactor / toFactor; // Returns the conversion factor ratio
}
// Function to convert a value from one unit to another
export function convertUnit(
value: number,
fromUnitId?: string,
toUnitId?: string,
): number | null {
let fromUnit: string | undefined;
let toUnit: string | undefined;
// Finds the category that contains the specified units and extracts fromUnit and toUnit using array methods
const category = dataTypeCategories.find((category) =>
category.formats.some((format) => {
if (format.id === fromUnitId) {
fromUnit = format.id;
}
if (format.id === toUnitId) {
toUnit = format.id;
}
return fromUnit && toUnit; // Break out early if both units are found
}),
);
if (!category || !fromUnit || !toUnit) {
if (!fromUnitId || !toUnitId) {
return null;
} // Return null if category or units are not found
}
// Gets the conversion factor for the specified units
const conversionFactor = getConversionFactor(
fromUnit,
toUnit,
category.name as any,
);
if (conversionFactor === null) {
const fromCategory = getCategoryName(fromUnitId);
const toCategory = getCategoryName(toUnitId);
// If either unit is unknown or the categories don't match, the conversion is invalid
if (!fromCategory || !toCategory || fromCategory !== toCategory) {
return null;
} // Return null if conversion is not possible
}
return value * conversionFactor;
// Delegate the actual numeric conversion (or identity) to the shared helper,
// which understands both Grafana-style and universal unit IDs.
return convertValue(value, fromUnitId, toUnitId);
}
// Function to get the category name for a given unit ID
export const getCategoryName = (unitId: string): CategoryNames | null => {
// Finds the category that contains the specified unit ID
const foundCategory = dataTypeCategories.find((category) =>
category.formats.some((format) => format.id === unitId),
);
return foundCategory ? (foundCategory.name as CategoryNames) : null;
};

View File

@@ -2,6 +2,9 @@ import { Layout } from 'react-grid-layout';
import { DefaultOptionType } from 'antd/es/select';
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
import { PrecisionOptionsEnum } from 'components/Graph/types';
import { YAxisCategoryNames } from 'components/YAxisUnitSelector/constants';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';
import {
initialQueryBuilderFormValuesMap,
PANEL_TYPES,
@@ -21,11 +24,7 @@ import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import {
dataTypeCategories,
getCategoryName,
} from './RightContainer/dataFormatCategories';
import { CategoryNames } from './RightContainer/types';
import { getCategoryName } from './RightContainer/dataFormatCategories';
export const getIsQueryModified = (
currentQuery: Query,
@@ -606,14 +605,21 @@ export const PANEL_TYPE_TO_QUERY_TYPES: Record<PANEL_TYPES, EQueryType[]> = {
* the label and value for each format.
*/
export const getCategorySelectOptionByName = (
name?: CategoryNames | string,
): DefaultOptionType[] =>
dataTypeCategories
.find((category) => category.name === name)
?.formats.map((format) => ({
label: format.name,
value: format.id,
})) || [];
name?: YAxisCategoryNames,
): DefaultOptionType[] => {
const categories = getYAxisCategories(YAxisSource.DASHBOARDS);
if (!categories.length) {
return [];
}
return (
categories
.find((category) => category.name === name)
?.units.map((unit) => ({
label: unit.name,
value: unit.id,
})) || []
);
};
/**
* Generates unit options based on the provided column unit.

View File

@@ -86,9 +86,9 @@ export default function OnboardingIngestionDetails(): JSX.Element {
<div className="ingestion-endpoint-section-error-container">
<Typography.Text className="ingestion-endpoint-section-error-text error">
<TriangleAlert size={14} />{' '}
{(error as AxiosError<RenderErrorResponseDTO>)?.response?.data?.error
?.message ||
(error as AxiosError)?.message ||
{((error as unknown) as AxiosError<RenderErrorResponseDTO>)?.response
?.data?.error.message ||
((error as unknown) as AxiosError)?.message ||
'Something went wrong'}
</Typography.Text>

View File

@@ -110,7 +110,7 @@ function AuthDomain(): JSX.Element {
let errorResult: APIError | null = null;
try {
ErrorResponseHandlerV2(
errorFetchingAuthDomainListResponse as AxiosError<ErrorV2Resp>,
(errorFetchingAuthDomainListResponse as unknown) as AxiosError<ErrorV2Resp>,
);
} catch (error) {
errorResult = error as APIError;

View File

@@ -1,10 +1,13 @@
import { CategoryNames } from 'container/NewWidget/RightContainer/types';
import { YAxisCategoryNames } from 'components/YAxisUnitSelector/constants';
export const categoryToSupport = [
CategoryNames.Data,
CategoryNames.DataRate,
CategoryNames.Time,
CategoryNames.Throughput,
CategoryNames.Miscellaneous,
CategoryNames.Boolean,
export const categoryToSupport: YAxisCategoryNames[] = [
YAxisCategoryNames.None,
YAxisCategoryNames.Data,
YAxisCategoryNames.DataRate,
YAxisCategoryNames.Time,
YAxisCategoryNames.Count,
YAxisCategoryNames.Operations,
YAxisCategoryNames.Percentage,
YAxisCategoryNames.Miscellaneous,
YAxisCategoryNames.Boolean,
];

View File

@@ -6,8 +6,8 @@ import (
)
type JSON struct {
Code string `json:"code"`
Message string `json:"message"`
Code string `json:"code" required:"true"`
Message string `json:"message" required:"true"`
Url string `json:"url,omitempty"`
Errors []responseerroradditional `json:"errors,omitempty"`
}

View File

@@ -16,13 +16,13 @@ const (
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type SuccessResponse struct {
Status string `json:"status"`
Data interface{} `json:"data,omitempty"`
Status string `json:"status" required:"true"`
Data interface{} `json:"data,omitempty" required:"true"`
}
type ErrorResponse struct {
Status string `json:"status"`
Error *errors.JSON `json:"error"`
Status string `json:"status" required:"true"`
Error *errors.JSON `json:"error" required:"true"`
}
func Success(rw http.ResponseWriter, httpCode int, data interface{}) {