mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-05 01:40:33 +01:00
Compare commits
15 Commits
feat/toolt
...
fix/rule-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed8917f8e6 | ||
|
|
43b7fea253 | ||
|
|
d0080abc8c | ||
|
|
0bb29f9e7f | ||
|
|
d2130b4d44 | ||
|
|
ff36066b07 | ||
|
|
e78a111494 | ||
|
|
f1af8f242c | ||
|
|
b4412c02b6 | ||
|
|
8409a9798d | ||
|
|
de6e4890ae | ||
|
|
20dd264ac1 | ||
|
|
8a7793794d | ||
|
|
680bcd08c3 | ||
|
|
5cf0e0fbb9 |
@@ -13,6 +13,8 @@ global:
|
||||
ingestion_url: <unset>
|
||||
# the url of the SigNoz MCP server. when unset, the MCP settings page is hidden in the frontend.
|
||||
# mcp_url: <unset>
|
||||
# the url of the SigNoz AI Assistant server. when unset, the AI Assistant is hidden in the frontend.
|
||||
# ai_assistant_url: <unset>
|
||||
|
||||
##################### Version #####################
|
||||
version:
|
||||
|
||||
@@ -96,6 +96,122 @@ components:
|
||||
- createdAt
|
||||
- updatedAt
|
||||
type: object
|
||||
AlertmanagertypesPostableChannel:
|
||||
oneOf:
|
||||
- required:
|
||||
- discord_configs
|
||||
- required:
|
||||
- email_configs
|
||||
- required:
|
||||
- incidentio_configs
|
||||
- required:
|
||||
- pagerduty_configs
|
||||
- required:
|
||||
- slack_configs
|
||||
- required:
|
||||
- webhook_configs
|
||||
- required:
|
||||
- opsgenie_configs
|
||||
- required:
|
||||
- wechat_configs
|
||||
- required:
|
||||
- pushover_configs
|
||||
- required:
|
||||
- victorops_configs
|
||||
- required:
|
||||
- sns_configs
|
||||
- required:
|
||||
- telegram_configs
|
||||
- required:
|
||||
- webex_configs
|
||||
- required:
|
||||
- msteams_configs
|
||||
- required:
|
||||
- msteamsv2_configs
|
||||
- required:
|
||||
- jira_configs
|
||||
- required:
|
||||
- rocketchat_configs
|
||||
- required:
|
||||
- mattermost_configs
|
||||
properties:
|
||||
discord_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigDiscordConfig'
|
||||
type: array
|
||||
email_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigEmailConfig'
|
||||
type: array
|
||||
incidentio_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigIncidentioConfig'
|
||||
type: array
|
||||
jira_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigJiraConfig'
|
||||
type: array
|
||||
mattermost_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMattermostConfig'
|
||||
type: array
|
||||
msteams_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMSTeamsConfig'
|
||||
type: array
|
||||
msteamsv2_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMSTeamsV2Config'
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
opsgenie_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigOpsGenieConfig'
|
||||
type: array
|
||||
pagerduty_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigPagerdutyConfig'
|
||||
type: array
|
||||
pushover_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigPushoverConfig'
|
||||
type: array
|
||||
rocketchat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigRocketchatConfig'
|
||||
type: array
|
||||
slack_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigSlackConfig'
|
||||
type: array
|
||||
sns_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigSNSConfig'
|
||||
type: array
|
||||
telegram_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigTelegramConfig'
|
||||
type: array
|
||||
victorops_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigVictorOpsConfig'
|
||||
type: array
|
||||
webex_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWebexConfig'
|
||||
type: array
|
||||
webhook_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWebhookConfig'
|
||||
type: array
|
||||
wechat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWechatConfig'
|
||||
type: array
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
AlertmanagertypesPostableRoutePolicy:
|
||||
properties:
|
||||
channels:
|
||||
@@ -133,6 +249,10 @@ components:
|
||||
type: string
|
||||
type: object
|
||||
AuthtypesAuthDomainConfig:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/AuthtypesSamlConfig'
|
||||
- $ref: '#/components/schemas/AuthtypesGoogleConfig'
|
||||
- $ref: '#/components/schemas/AuthtypesOIDCConfig'
|
||||
properties:
|
||||
googleAuthConfig:
|
||||
$ref: '#/components/schemas/AuthtypesGoogleConfig'
|
||||
@@ -145,8 +265,15 @@ components:
|
||||
ssoEnabled:
|
||||
type: boolean
|
||||
ssoType:
|
||||
type: string
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
type: object
|
||||
AuthtypesAuthNProvider:
|
||||
enum:
|
||||
- google_auth
|
||||
- saml
|
||||
- email_password
|
||||
- oidc
|
||||
type: string
|
||||
AuthtypesAuthNProviderInfo:
|
||||
properties:
|
||||
relayStatePath:
|
||||
@@ -169,7 +296,7 @@ components:
|
||||
AuthtypesCallbackAuthNSupport:
|
||||
properties:
|
||||
provider:
|
||||
type: string
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
@@ -177,27 +304,17 @@ components:
|
||||
properties:
|
||||
authNProviderInfo:
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProviderInfo'
|
||||
config:
|
||||
$ref: '#/components/schemas/AuthtypesAuthDomainConfig'
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
googleAuthConfig:
|
||||
$ref: '#/components/schemas/AuthtypesGoogleConfig'
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
oidcConfig:
|
||||
$ref: '#/components/schemas/AuthtypesOIDCConfig'
|
||||
orgId:
|
||||
type: string
|
||||
roleMapping:
|
||||
$ref: '#/components/schemas/AuthtypesRoleMapping'
|
||||
samlConfig:
|
||||
$ref: '#/components/schemas/AuthtypesSamlConfig'
|
||||
ssoEnabled:
|
||||
type: boolean
|
||||
ssoType:
|
||||
type: string
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
@@ -323,7 +440,7 @@ components:
|
||||
AuthtypesPasswordAuthNSupport:
|
||||
properties:
|
||||
provider:
|
||||
type: string
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
type: object
|
||||
AuthtypesPatchableObjects:
|
||||
properties:
|
||||
@@ -458,7 +575,7 @@ components:
|
||||
- relation
|
||||
- object
|
||||
type: object
|
||||
AuthtypesUpdateableAuthDomain:
|
||||
AuthtypesUpdatableAuthDomain:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/components/schemas/AuthtypesAuthDomainConfig'
|
||||
@@ -2363,6 +2480,9 @@ components:
|
||||
type: object
|
||||
GlobaltypesConfig:
|
||||
properties:
|
||||
ai_assistant_url:
|
||||
nullable: true
|
||||
type: string
|
||||
external_url:
|
||||
type: string
|
||||
identN:
|
||||
@@ -2376,6 +2496,7 @@ components:
|
||||
- external_url
|
||||
- ingestion_url
|
||||
- mcp_url
|
||||
- ai_assistant_url
|
||||
type: object
|
||||
GlobaltypesIdentNConfig:
|
||||
properties:
|
||||
@@ -4227,19 +4348,20 @@ components:
|
||||
$ref: '#/components/schemas/RuletypesEvaluationKind'
|
||||
spec:
|
||||
$ref: '#/components/schemas/RuletypesCumulativeWindow'
|
||||
type: object
|
||||
RuletypesEvaluationEnvelope:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/RuletypesEvaluationRolling'
|
||||
- $ref: '#/components/schemas/RuletypesEvaluationCumulative'
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/RuletypesEvaluationKind'
|
||||
spec: {}
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
RuletypesEvaluationEnvelope:
|
||||
discriminator:
|
||||
mapping:
|
||||
cumulative: '#/components/schemas/RuletypesEvaluationCumulative'
|
||||
rolling: '#/components/schemas/RuletypesEvaluationRolling'
|
||||
propertyName: kind
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/RuletypesEvaluationRolling'
|
||||
- $ref: '#/components/schemas/RuletypesEvaluationCumulative'
|
||||
type: object
|
||||
RuletypesEvaluationKind:
|
||||
enum:
|
||||
- rolling
|
||||
@@ -4251,6 +4373,9 @@ components:
|
||||
$ref: '#/components/schemas/RuletypesEvaluationKind'
|
||||
spec:
|
||||
$ref: '#/components/schemas/RuletypesRollingWindow'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
RuletypesGettableTestRule:
|
||||
properties:
|
||||
@@ -4558,15 +4683,12 @@ components:
|
||||
- compositeQuery
|
||||
type: object
|
||||
RuletypesRuleThresholdData:
|
||||
discriminator:
|
||||
mapping:
|
||||
basic: '#/components/schemas/RuletypesThresholdBasic'
|
||||
propertyName: kind
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/RuletypesThresholdBasic'
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/RuletypesThresholdKind'
|
||||
spec: {}
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
RuletypesRuleType:
|
||||
enum:
|
||||
@@ -4608,6 +4730,9 @@ components:
|
||||
$ref: '#/components/schemas/RuletypesThresholdKind'
|
||||
spec:
|
||||
$ref: '#/components/schemas/RuletypesBasicRuleThresholds'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
RuletypesThresholdKind:
|
||||
enum:
|
||||
@@ -5665,7 +5790,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ConfigReceiver'
|
||||
$ref: '#/components/schemas/AlertmanagertypesPostableChannel'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
@@ -6944,20 +7069,20 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthtypesPostableAuthDomain'
|
||||
responses:
|
||||
"200":
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AuthtypesGettableAuthDomain'
|
||||
$ref: '#/components/schemas/TypesIdentifiable'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
description: Created
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
@@ -7042,6 +7167,63 @@ paths:
|
||||
summary: Delete auth domain
|
||||
tags:
|
||||
- authdomains
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns an auth domain by ID
|
||||
operationId: GetAuthDomain
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AuthtypesGettableAuthDomain'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Get auth domain by ID
|
||||
tags:
|
||||
- authdomains
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint updates an auth domain
|
||||
@@ -7056,7 +7238,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthtypesUpdateableAuthDomain'
|
||||
$ref: '#/components/schemas/AuthtypesUpdatableAuthDomain'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
"ts-jest": "29.4.6",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript-plugin-css-modules": "5.2.0",
|
||||
"use-sync-external-store": "1.6.0",
|
||||
"vite-plugin-checker": "0.12.0",
|
||||
"vite-plugin-compression": "0.5.1",
|
||||
"vite-plugin-image-optimizer": "2.0.3",
|
||||
|
||||
@@ -19,9 +19,11 @@ import type {
|
||||
|
||||
import type {
|
||||
AuthtypesPostableAuthDomainDTO,
|
||||
AuthtypesUpdateableAuthDomainDTO,
|
||||
CreateAuthDomain200,
|
||||
AuthtypesUpdatableAuthDomainDTO,
|
||||
CreateAuthDomain201,
|
||||
DeleteAuthDomainPathParameters,
|
||||
GetAuthDomain200,
|
||||
GetAuthDomainPathParameters,
|
||||
ListAuthDomains200,
|
||||
RenderErrorResponseDTO,
|
||||
UpdateAuthDomainPathParameters,
|
||||
@@ -124,7 +126,7 @@ export const createAuthDomain = (
|
||||
authtypesPostableAuthDomainDTO: BodyType<AuthtypesPostableAuthDomainDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateAuthDomain200>({
|
||||
return GeneratedAPIInstance<CreateAuthDomain201>({
|
||||
url: `/api/v1/domains`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -277,19 +279,122 @@ export const useDeleteAuthDomain = <
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint returns an auth domain by ID
|
||||
* @summary Get auth domain by ID
|
||||
*/
|
||||
export const getAuthDomain = (
|
||||
{ id }: GetAuthDomainPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetAuthDomain200>({
|
||||
url: `/api/v1/domains/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetAuthDomainQueryKey = ({
|
||||
id,
|
||||
}: GetAuthDomainPathParameters) => {
|
||||
return [`/api/v1/domains/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetAuthDomainQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getAuthDomain>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetAuthDomainPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getAuthDomain>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetAuthDomainQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getAuthDomain>>> = ({
|
||||
signal,
|
||||
}) => getAuthDomain({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getAuthDomain>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetAuthDomainQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getAuthDomain>>
|
||||
>;
|
||||
export type GetAuthDomainQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get auth domain by ID
|
||||
*/
|
||||
|
||||
export function useGetAuthDomain<
|
||||
TData = Awaited<ReturnType<typeof getAuthDomain>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetAuthDomainPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getAuthDomain>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetAuthDomainQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get auth domain by ID
|
||||
*/
|
||||
export const invalidateGetAuthDomain = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetAuthDomainPathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetAuthDomainQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint updates an auth domain
|
||||
* @summary Update auth domain
|
||||
*/
|
||||
export const updateAuthDomain = (
|
||||
{ id }: UpdateAuthDomainPathParameters,
|
||||
authtypesUpdateableAuthDomainDTO: BodyType<AuthtypesUpdateableAuthDomainDTO>,
|
||||
authtypesUpdatableAuthDomainDTO: BodyType<AuthtypesUpdatableAuthDomainDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/domains/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: authtypesUpdateableAuthDomainDTO,
|
||||
data: authtypesUpdatableAuthDomainDTO,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -302,7 +407,7 @@ export const getUpdateAuthDomainMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateAuthDomainPathParameters;
|
||||
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
|
||||
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -311,7 +416,7 @@ export const getUpdateAuthDomainMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateAuthDomainPathParameters;
|
||||
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
|
||||
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
@@ -328,7 +433,7 @@ export const getUpdateAuthDomainMutationOptions = <
|
||||
Awaited<ReturnType<typeof updateAuthDomain>>,
|
||||
{
|
||||
pathParams: UpdateAuthDomainPathParameters;
|
||||
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
|
||||
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
@@ -343,7 +448,7 @@ export type UpdateAuthDomainMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateAuthDomain>>
|
||||
>;
|
||||
export type UpdateAuthDomainMutationBody =
|
||||
BodyType<AuthtypesUpdateableAuthDomainDTO>;
|
||||
BodyType<AuthtypesUpdatableAuthDomainDTO>;
|
||||
export type UpdateAuthDomainMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
@@ -358,7 +463,7 @@ export const useUpdateAuthDomain = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateAuthDomainPathParameters;
|
||||
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
|
||||
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -367,7 +472,7 @@ export const useUpdateAuthDomain = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateAuthDomainPathParameters;
|
||||
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
|
||||
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
AlertmanagertypesPostableChannelDTO,
|
||||
ConfigReceiverDTO,
|
||||
CreateChannel201,
|
||||
DeleteChannelByIDPathParameters,
|
||||
@@ -122,14 +123,14 @@ export const invalidateListChannels = async (
|
||||
* @summary Create notification channel
|
||||
*/
|
||||
export const createChannel = (
|
||||
configReceiverDTO: BodyType<ConfigReceiverDTO>,
|
||||
alertmanagertypesPostableChannelDTO: BodyType<AlertmanagertypesPostableChannelDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateChannel201>({
|
||||
url: `/api/v1/channels`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: configReceiverDTO,
|
||||
data: alertmanagertypesPostableChannelDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
@@ -141,13 +142,13 @@ export const getCreateChannelMutationOptions = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createChannel>>,
|
||||
TError,
|
||||
{ data: BodyType<ConfigReceiverDTO> },
|
||||
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createChannel>>,
|
||||
TError,
|
||||
{ data: BodyType<ConfigReceiverDTO> },
|
||||
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createChannel'];
|
||||
@@ -161,7 +162,7 @@ export const getCreateChannelMutationOptions = <
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createChannel>>,
|
||||
{ data: BodyType<ConfigReceiverDTO> }
|
||||
{ data: BodyType<AlertmanagertypesPostableChannelDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
@@ -174,7 +175,8 @@ export const getCreateChannelMutationOptions = <
|
||||
export type CreateChannelMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createChannel>>
|
||||
>;
|
||||
export type CreateChannelMutationBody = BodyType<ConfigReceiverDTO>;
|
||||
export type CreateChannelMutationBody =
|
||||
BodyType<AlertmanagertypesPostableChannelDTO>;
|
||||
export type CreateChannelMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
@@ -187,13 +189,13 @@ export const useCreateChannel = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createChannel>>,
|
||||
TError,
|
||||
{ data: BodyType<ConfigReceiverDTO> },
|
||||
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createChannel>>,
|
||||
TError,
|
||||
{ data: BodyType<ConfigReceiverDTO> },
|
||||
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getCreateChannelMutationOptions(options);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
getUserExpressionFromCombined,
|
||||
} from '../utils';
|
||||
import { QuerySearchV2Context } from './context';
|
||||
import type { QuerySearchV2ContextValue } from './QuerySearchV2.store';
|
||||
import { createExpressionStore } from './QuerySearchV2.store';
|
||||
|
||||
export interface QuerySearchV2ProviderProps {
|
||||
queryParamKey: string;
|
||||
initialExpression?: string;
|
||||
/**
|
||||
* @default false
|
||||
*/
|
||||
persistOnUnmount?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider component that creates a scoped zustand store and exposes
|
||||
* expression state to children via context.
|
||||
*/
|
||||
export function QuerySearchV2Provider({
|
||||
initialExpression = '',
|
||||
persistOnUnmount = false,
|
||||
queryParamKey,
|
||||
children,
|
||||
}: QuerySearchV2ProviderProps): JSX.Element {
|
||||
const storeRef = useRef(createExpressionStore());
|
||||
const store = storeRef.current;
|
||||
|
||||
const [urlExpression, setUrlExpression] = useQueryState(
|
||||
queryParamKey,
|
||||
parseAsString,
|
||||
);
|
||||
|
||||
const committedExpression = useStore(store, (s) => s.committedExpression);
|
||||
const setInputExpression = useStore(store, (s) => s.setInputExpression);
|
||||
const commitExpression = useStore(store, (s) => s.commitExpression);
|
||||
const initializeFromUrl = useStore(store, (s) => s.initializeFromUrl);
|
||||
const resetExpression = useStore(store, (s) => s.resetExpression);
|
||||
|
||||
const isInitialized = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!isInitialized.current && urlExpression) {
|
||||
const cleanedExpression = getUserExpressionFromCombined(
|
||||
initialExpression,
|
||||
urlExpression,
|
||||
);
|
||||
initializeFromUrl(cleanedExpression);
|
||||
isInitialized.current = true;
|
||||
}
|
||||
}, [urlExpression, initialExpression, initializeFromUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized.current || !urlExpression) {
|
||||
setUrlExpression(committedExpression || null);
|
||||
}
|
||||
}, [committedExpression, setUrlExpression, urlExpression]);
|
||||
|
||||
useEffect(() => {
|
||||
return (): void => {
|
||||
if (!persistOnUnmount) {
|
||||
setUrlExpression(null);
|
||||
resetExpression();
|
||||
}
|
||||
};
|
||||
}, [persistOnUnmount, setUrlExpression, resetExpression]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(expression: string): void => {
|
||||
const userOnly = getUserExpressionFromCombined(
|
||||
initialExpression,
|
||||
expression,
|
||||
);
|
||||
setInputExpression(userOnly);
|
||||
},
|
||||
[initialExpression, setInputExpression],
|
||||
);
|
||||
|
||||
const handleRun = useCallback(
|
||||
(expression: string): void => {
|
||||
const userOnly = getUserExpressionFromCombined(
|
||||
initialExpression,
|
||||
expression,
|
||||
);
|
||||
commitExpression(userOnly);
|
||||
},
|
||||
[initialExpression, commitExpression],
|
||||
);
|
||||
|
||||
const combinedExpression = useMemo(
|
||||
() => combineInitialAndUserExpression(initialExpression, committedExpression),
|
||||
[initialExpression, committedExpression],
|
||||
);
|
||||
|
||||
const contextValue = useMemo<QuerySearchV2ContextValue>(
|
||||
() => ({
|
||||
expression: combinedExpression,
|
||||
userExpression: committedExpression,
|
||||
initialExpression,
|
||||
querySearchProps: {
|
||||
initialExpression: initialExpression.trim() ? initialExpression : undefined,
|
||||
onChange: handleChange,
|
||||
onRun: handleRun,
|
||||
},
|
||||
}),
|
||||
[
|
||||
combinedExpression,
|
||||
committedExpression,
|
||||
initialExpression,
|
||||
handleChange,
|
||||
handleRun,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<QuerySearchV2Context.Provider value={contextValue}>
|
||||
{children}
|
||||
</QuerySearchV2Context.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { createStore, StoreApi } from 'zustand';
|
||||
|
||||
export type QuerySearchV2Store = {
|
||||
/**
|
||||
* User-typed expression (local state, updates on typing)
|
||||
*/
|
||||
inputExpression: string;
|
||||
/**
|
||||
* Committed expression (synced to URL, updates on submit)
|
||||
*/
|
||||
committedExpression: string;
|
||||
setInputExpression: (expression: string) => void;
|
||||
commitExpression: (expression: string) => void;
|
||||
resetExpression: () => void;
|
||||
initializeFromUrl: (urlExpression: string) => void;
|
||||
};
|
||||
|
||||
export interface QuerySearchProps {
|
||||
initialExpression: string | undefined;
|
||||
onChange: (expression: string) => void;
|
||||
onRun: (expression: string) => void;
|
||||
}
|
||||
|
||||
export interface QuerySearchV2ContextValue {
|
||||
/**
|
||||
* Combined expression: "initialExpression AND (userExpression)"
|
||||
*/
|
||||
expression: string;
|
||||
userExpression: string;
|
||||
initialExpression: string;
|
||||
querySearchProps: QuerySearchProps;
|
||||
}
|
||||
|
||||
export function createExpressionStore(): StoreApi<QuerySearchV2Store> {
|
||||
return createStore<QuerySearchV2Store>((set) => ({
|
||||
inputExpression: '',
|
||||
committedExpression: '',
|
||||
setInputExpression: (expression: string): void => {
|
||||
set({ inputExpression: expression });
|
||||
},
|
||||
commitExpression: (expression: string): void => {
|
||||
set({
|
||||
inputExpression: expression,
|
||||
committedExpression: expression,
|
||||
});
|
||||
},
|
||||
resetExpression: (): void => {
|
||||
set({
|
||||
inputExpression: '',
|
||||
committedExpression: '',
|
||||
});
|
||||
},
|
||||
initializeFromUrl: (urlExpression: string): void => {
|
||||
set({
|
||||
inputExpression: urlExpression,
|
||||
committedExpression: urlExpression,
|
||||
});
|
||||
},
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
|
||||
import { useQuerySearchV2Context } from '../context';
|
||||
import {
|
||||
QuerySearchV2Provider,
|
||||
QuerySearchV2ProviderProps,
|
||||
} from '../QuerySearchV2.provider';
|
||||
|
||||
const mockSetQueryState = jest.fn();
|
||||
let mockUrlValue: string | null = null;
|
||||
|
||||
jest.mock('nuqs', () => ({
|
||||
parseAsString: {},
|
||||
useQueryState: jest.fn(() => [mockUrlValue, mockSetQueryState]),
|
||||
}));
|
||||
|
||||
function createWrapper(
|
||||
props: Partial<QuerySearchV2ProviderProps> = {},
|
||||
): ({ children }: { children: ReactNode }) => JSX.Element {
|
||||
return function Wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
return (
|
||||
<QuerySearchV2Provider queryParamKey="testExpression" {...props}>
|
||||
{children}
|
||||
</QuerySearchV2Provider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
describe('QuerySearchExpressionProvider', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUrlValue = null;
|
||||
});
|
||||
|
||||
it('should provide initial context values', () => {
|
||||
const { result } = renderHook(() => useQuerySearchV2Context(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.expression).toBe('');
|
||||
expect(result.current.userExpression).toBe('');
|
||||
expect(result.current.initialExpression).toBe('');
|
||||
});
|
||||
|
||||
it('should combine initialExpression with userExpression', () => {
|
||||
const { result } = renderHook(() => useQuerySearchV2Context(), {
|
||||
wrapper: createWrapper({ initialExpression: 'k8s.pod.name = "my-pod"' }),
|
||||
});
|
||||
|
||||
expect(result.current.expression).toBe('k8s.pod.name = "my-pod"');
|
||||
expect(result.current.initialExpression).toBe('k8s.pod.name = "my-pod"');
|
||||
|
||||
act(() => {
|
||||
result.current.querySearchProps.onChange('service = "api"');
|
||||
});
|
||||
act(() => {
|
||||
result.current.querySearchProps.onRun('service = "api"');
|
||||
});
|
||||
|
||||
expect(result.current.expression).toBe(
|
||||
'k8s.pod.name = "my-pod" AND (service = "api")',
|
||||
);
|
||||
expect(result.current.userExpression).toBe('service = "api"');
|
||||
});
|
||||
|
||||
it('should provide querySearchProps with correct callbacks', () => {
|
||||
const { result } = renderHook(() => useQuerySearchV2Context(), {
|
||||
wrapper: createWrapper({ initialExpression: 'initial' }),
|
||||
});
|
||||
|
||||
expect(result.current.querySearchProps.initialExpression).toBe('initial');
|
||||
expect(typeof result.current.querySearchProps.onChange).toBe('function');
|
||||
expect(typeof result.current.querySearchProps.onRun).toBe('function');
|
||||
});
|
||||
|
||||
it('should initialize from URL value on mount', () => {
|
||||
mockUrlValue = 'status = 500';
|
||||
|
||||
const { result } = renderHook(() => useQuerySearchV2Context(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.userExpression).toBe('status = 500');
|
||||
expect(result.current.expression).toBe('status = 500');
|
||||
});
|
||||
|
||||
it('should throw error when used outside provider', () => {
|
||||
expect(() => {
|
||||
renderHook(() => useQuerySearchV2Context());
|
||||
}).toThrow(
|
||||
'useQuerySearchV2Context must be used within a QuerySearchV2Provider',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { createExpressionStore } from '../QuerySearchV2.store';
|
||||
|
||||
describe('createExpressionStore', () => {
|
||||
it('should create a store with initial state', () => {
|
||||
const store = createExpressionStore();
|
||||
const state = store.getState();
|
||||
|
||||
expect(state.inputExpression).toBe('');
|
||||
expect(state.committedExpression).toBe('');
|
||||
});
|
||||
|
||||
it('should update inputExpression via setInputExpression', () => {
|
||||
const store = createExpressionStore();
|
||||
|
||||
store.getState().setInputExpression('service.name = "api"');
|
||||
|
||||
expect(store.getState().inputExpression).toBe('service.name = "api"');
|
||||
expect(store.getState().committedExpression).toBe('');
|
||||
});
|
||||
|
||||
it('should update both expressions via commitExpression', () => {
|
||||
const store = createExpressionStore();
|
||||
|
||||
store.getState().setInputExpression('service.name = "api"');
|
||||
store.getState().commitExpression('service.name = "api"');
|
||||
|
||||
expect(store.getState().inputExpression).toBe('service.name = "api"');
|
||||
expect(store.getState().committedExpression).toBe('service.name = "api"');
|
||||
});
|
||||
|
||||
it('should reset all state via resetExpression', () => {
|
||||
const store = createExpressionStore();
|
||||
|
||||
store.getState().setInputExpression('service.name = "api"');
|
||||
store.getState().commitExpression('service.name = "api"');
|
||||
store.getState().resetExpression();
|
||||
|
||||
expect(store.getState().inputExpression).toBe('');
|
||||
expect(store.getState().committedExpression).toBe('');
|
||||
});
|
||||
|
||||
it('should initialize from URL value', () => {
|
||||
const store = createExpressionStore();
|
||||
|
||||
store.getState().initializeFromUrl('status = 500');
|
||||
|
||||
expect(store.getState().inputExpression).toBe('status = 500');
|
||||
expect(store.getState().committedExpression).toBe('status = 500');
|
||||
});
|
||||
|
||||
it('should create isolated store instances', () => {
|
||||
const store1 = createExpressionStore();
|
||||
const store2 = createExpressionStore();
|
||||
|
||||
store1.getState().setInputExpression('expr1');
|
||||
store2.getState().setInputExpression('expr2');
|
||||
|
||||
expect(store1.getState().inputExpression).toBe('expr1');
|
||||
expect(store2.getState().inputExpression).toBe('expr2');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
// eslint-disable-next-line no-restricted-imports -- React Context required for scoped store pattern
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
import type { QuerySearchV2ContextValue } from './QuerySearchV2.store';
|
||||
|
||||
export const QuerySearchV2Context =
|
||||
createContext<QuerySearchV2ContextValue | null>(null);
|
||||
|
||||
export function useQuerySearchV2Context(): QuerySearchV2ContextValue {
|
||||
const context = useContext(QuerySearchV2Context);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useQuerySearchV2Context must be used within a QuerySearchV2Provider',
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export { useQuerySearchV2Context } from './context';
|
||||
export type { QuerySearchV2ProviderProps } from './QuerySearchV2.provider';
|
||||
export { QuerySearchV2Provider } from './QuerySearchV2.provider';
|
||||
export type {
|
||||
QuerySearchProps,
|
||||
QuerySearchV2ContextValue,
|
||||
QuerySearchV2Store,
|
||||
} from './QuerySearchV2.store';
|
||||
@@ -19,6 +19,13 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.query-search-initial-scope-label {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.query-where-clause-editor {
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
@@ -53,6 +60,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hasInitialExpression .cm-editor .cm-content {
|
||||
padding-left: 22px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
@@ -68,7 +79,6 @@
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
padding: 0px !important;
|
||||
background-color: var(--l1-background) !important;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--l1-border);
|
||||
|
||||
@@ -30,7 +30,7 @@ import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariabl
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { debounce, isNull } from 'lodash-es';
|
||||
import { Info, TriangleAlert } from 'lucide-react';
|
||||
import { Filter, Info, TriangleAlert } from 'lucide-react';
|
||||
import {
|
||||
IDetailedError,
|
||||
IQueryContext,
|
||||
@@ -47,6 +47,7 @@ import { validateQuery } from 'utils/queryValidationUtils';
|
||||
import { unquote } from 'utils/stringUtils';
|
||||
|
||||
import { queryExamples } from './constants';
|
||||
import { combineInitialAndUserExpression } from './utils';
|
||||
|
||||
import './QuerySearch.styles.scss';
|
||||
|
||||
@@ -85,6 +86,8 @@ interface QuerySearchProps {
|
||||
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
|
||||
onRun?: (query: string) => void;
|
||||
showFilterSuggestionsWithoutMetric?: boolean;
|
||||
/** When set, the editor shows only the user expression; API/filter uses `initial AND (user)`. */
|
||||
initialExpression?: string;
|
||||
}
|
||||
|
||||
function QuerySearch({
|
||||
@@ -96,6 +99,7 @@ function QuerySearch({
|
||||
signalSource,
|
||||
hardcodedAttributeKeys,
|
||||
showFilterSuggestionsWithoutMetric,
|
||||
initialExpression,
|
||||
}: QuerySearchProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
|
||||
@@ -112,18 +116,26 @@ function QuerySearch({
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const editorRef = useRef<EditorView | null>(null);
|
||||
|
||||
const handleQueryValidation = useCallback((newExpression: string): void => {
|
||||
try {
|
||||
const validationResponse = validateQuery(newExpression);
|
||||
setValidation(validationResponse);
|
||||
} catch (error) {
|
||||
setValidation({
|
||||
isValid: false,
|
||||
message: 'Failed to process query',
|
||||
errors: [error as IDetailedError],
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
const isScopedFilter = initialExpression !== undefined;
|
||||
|
||||
const validateExpressionForEditor = useCallback(
|
||||
(editorDoc: string): void => {
|
||||
const toValidate = isScopedFilter
|
||||
? combineInitialAndUserExpression(initialExpression ?? '', editorDoc)
|
||||
: editorDoc;
|
||||
try {
|
||||
const validationResponse = validateQuery(toValidate);
|
||||
setValidation(validationResponse);
|
||||
} catch (error) {
|
||||
setValidation({
|
||||
isValid: false,
|
||||
message: 'Failed to process query',
|
||||
errors: [error as IDetailedError],
|
||||
});
|
||||
}
|
||||
},
|
||||
[initialExpression, isScopedFilter],
|
||||
);
|
||||
|
||||
const getCurrentExpression = useCallback(
|
||||
(): string => editorRef.current?.state.doc.toString() || '',
|
||||
@@ -165,6 +177,8 @@ function QuerySearch({
|
||||
setIsEditorReady(true);
|
||||
}, []);
|
||||
|
||||
const prevQueryDataExpressionRef = useRef<string | undefined>();
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!isEditorReady) {
|
||||
@@ -173,13 +187,22 @@ function QuerySearch({
|
||||
|
||||
const newExpression = queryData.filter?.expression || '';
|
||||
const currentExpression = getCurrentExpression();
|
||||
const prevExpression = prevQueryDataExpressionRef.current;
|
||||
|
||||
// Do not update codemirror editor if the expression is the same
|
||||
if (newExpression !== currentExpression && !isFocused) {
|
||||
// Only sync editor when queryData.filter?.expression actually changed from external source
|
||||
// Not when focus changed (which would reset uncommitted user input)
|
||||
const queryDataExpressionChanged = prevExpression !== newExpression;
|
||||
prevQueryDataExpressionRef.current = newExpression;
|
||||
|
||||
if (
|
||||
queryDataExpressionChanged &&
|
||||
newExpression !== currentExpression &&
|
||||
!isFocused
|
||||
) {
|
||||
updateEditorValue(newExpression, { skipOnChange: true });
|
||||
if (newExpression) {
|
||||
handleQueryValidation(newExpression);
|
||||
}
|
||||
}
|
||||
if (!isFocused) {
|
||||
validateExpressionForEditor(currentExpression);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -284,7 +307,7 @@ function QuerySearch({
|
||||
}
|
||||
});
|
||||
}
|
||||
setKeySuggestions(Array.from(merged.values()));
|
||||
setKeySuggestions([...merged.values()]);
|
||||
|
||||
// Force reopen the completion if editor is available and focused
|
||||
if (editorRef.current) {
|
||||
@@ -337,7 +360,7 @@ function QuerySearch({
|
||||
// If value contains single quotes, escape them and wrap in single quotes
|
||||
if (value.includes("'")) {
|
||||
// Replace single quotes with escaped single quotes
|
||||
const escapedValue = value.replace(/'/g, "\\'");
|
||||
const escapedValue = value.replaceAll(/'/g, "\\'");
|
||||
return `'${escapedValue}'`;
|
||||
}
|
||||
|
||||
@@ -614,7 +637,7 @@ function QuerySearch({
|
||||
|
||||
const handleBlur = (): void => {
|
||||
const currentExpression = getCurrentExpression();
|
||||
handleQueryValidation(currentExpression);
|
||||
validateExpressionForEditor(currentExpression);
|
||||
setIsFocused(false);
|
||||
};
|
||||
|
||||
@@ -632,7 +655,6 @@ function QuerySearch({
|
||||
);
|
||||
|
||||
const handleExampleClick = (exampleQuery: string): void => {
|
||||
// If there's an existing query, append the example with AND
|
||||
const currentExpression = getCurrentExpression();
|
||||
const newExpression = currentExpression
|
||||
? `${currentExpression} AND ${exampleQuery}`
|
||||
@@ -897,12 +919,12 @@ function QuerySearch({
|
||||
|
||||
// If we have previous pairs, we can prioritize keys that haven't been used yet
|
||||
if (queryContext.queryPairs && queryContext.queryPairs.length > 0) {
|
||||
const usedKeys = queryContext.queryPairs.map((pair) => pair.key);
|
||||
const usedKeys = new Set(queryContext.queryPairs.map((pair) => pair.key));
|
||||
|
||||
// Add boost to unused keys to prioritize them
|
||||
options = options.map((option) => ({
|
||||
...option,
|
||||
boost: usedKeys.includes(option.label) ? -10 : 10,
|
||||
boost: usedKeys.has(option.label) ? -10 : 10,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1317,6 +1339,19 @@ function QuerySearch({
|
||||
)}
|
||||
|
||||
<div className="query-where-clause-editor-container">
|
||||
{isScopedFilter ? (
|
||||
<Tooltip title={initialExpression || ''} placement="left">
|
||||
<div className="query-search-initial-scope-label">
|
||||
<Filter
|
||||
size={14}
|
||||
style={{
|
||||
opacity: 0.9,
|
||||
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
<Tooltip
|
||||
title={<div data-log-detail-ignore="true">{getTooltipContent()}</div>}
|
||||
placement="left"
|
||||
@@ -1356,6 +1391,7 @@ function QuerySearch({
|
||||
className={cx('query-where-clause-editor', {
|
||||
isValid: validation.isValid === true,
|
||||
hasErrors: validation.errors.length > 0,
|
||||
hasInitialExpression: isScopedFilter,
|
||||
})}
|
||||
extensions={[
|
||||
autocompletion({
|
||||
@@ -1390,7 +1426,12 @@ function QuerySearch({
|
||||
// Mod-Enter is usually Ctrl-Enter or Cmd-Enter based on OS
|
||||
run: (): boolean => {
|
||||
if (onRun && typeof onRun === 'function') {
|
||||
onRun(getCurrentExpression());
|
||||
const user = getCurrentExpression();
|
||||
onRun(
|
||||
isScopedFilter
|
||||
? combineInitialAndUserExpression(initialExpression ?? '', user)
|
||||
: user,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@@ -1555,6 +1596,7 @@ QuerySearch.defaultProps = {
|
||||
placeholder:
|
||||
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')",
|
||||
showFilterSuggestionsWithoutMetric: false,
|
||||
initialExpression: undefined,
|
||||
};
|
||||
|
||||
export default QuerySearch;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
getUserExpressionFromCombined,
|
||||
} from '../utils';
|
||||
|
||||
describe('entityLogsExpression', () => {
|
||||
describe('combineInitialAndUserExpression', () => {
|
||||
it('returns user when initial is empty', () => {
|
||||
expect(combineInitialAndUserExpression('', 'body contains error')).toBe(
|
||||
'body contains error',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns initial when user is empty', () => {
|
||||
expect(combineInitialAndUserExpression('k8s.pod.name = "x"', '')).toBe(
|
||||
'k8s.pod.name = "x"',
|
||||
);
|
||||
});
|
||||
|
||||
it('wraps user in parentheses with AND', () => {
|
||||
expect(
|
||||
combineInitialAndUserExpression('k8s.pod.name = "x"', 'body = "a"'),
|
||||
).toBe('k8s.pod.name = "x" AND (body = "a")');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserExpressionFromCombined', () => {
|
||||
it('returns empty when combined equals initial', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined('k8s.pod.name = "x"', 'k8s.pod.name = "x"'),
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
it('extracts user from wrapped form', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined(
|
||||
'k8s.pod.name = "x"',
|
||||
'k8s.pod.name = "x" AND (body = "a")',
|
||||
),
|
||||
).toBe('body = "a"');
|
||||
});
|
||||
|
||||
it('extracts user from legacy AND without parens', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined(
|
||||
'k8s.pod.name = "x"',
|
||||
'k8s.pod.name = "x" AND body = "a"',
|
||||
),
|
||||
).toBe('body = "a"');
|
||||
});
|
||||
|
||||
it('returns full combined when initial is empty', () => {
|
||||
expect(getUserExpressionFromCombined('', 'service.name = "a"')).toBe(
|
||||
'service.name = "a"',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
export function combineInitialAndUserExpression(
|
||||
initial: string,
|
||||
user: string,
|
||||
): string {
|
||||
const i = initial.trim();
|
||||
const u = user.trim();
|
||||
if (!i) {
|
||||
return u;
|
||||
}
|
||||
if (!u) {
|
||||
return i;
|
||||
}
|
||||
return `${i} AND (${u})`;
|
||||
}
|
||||
|
||||
export function getUserExpressionFromCombined(
|
||||
initial: string,
|
||||
combined: string | null | undefined,
|
||||
): string {
|
||||
const i = initial.trim();
|
||||
const c = (combined ?? '').trim();
|
||||
if (!c) {
|
||||
return '';
|
||||
}
|
||||
if (!i) {
|
||||
return c;
|
||||
}
|
||||
if (c === i) {
|
||||
return '';
|
||||
}
|
||||
const wrappedPrefix = `${i} AND (`;
|
||||
if (c.startsWith(wrappedPrefix) && c.endsWith(')')) {
|
||||
return c.slice(wrappedPrefix.length, -1);
|
||||
}
|
||||
const plainPrefix = `${i} AND `;
|
||||
if (c.startsWith(plainPrefix)) {
|
||||
return c.slice(plainPrefix.length);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
14
frontend/src/components/QueryBuilderV2/index.ts
Normal file
14
frontend/src/components/QueryBuilderV2/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type {
|
||||
QuerySearchProps,
|
||||
QuerySearchV2ContextValue,
|
||||
QuerySearchV2ProviderProps,
|
||||
} from './QueryV2/QuerySearch/Provider';
|
||||
export {
|
||||
QuerySearchV2Provider,
|
||||
useQuerySearchV2Context,
|
||||
} from './QueryV2/QuerySearch/Provider';
|
||||
export { QueryBuilderV2 } from './QueryBuilderV2';
|
||||
export {
|
||||
QueryBuilderV2Provider,
|
||||
useQueryBuilderV2Context,
|
||||
} from './QueryBuilderV2Context';
|
||||
@@ -47,10 +47,16 @@ function TanStackCustomTableRow<TData>({
|
||||
const isActive = context?.isRowActive?.(rowData) ?? false;
|
||||
const extraClass = context?.getRowClassName?.(rowData) ?? '';
|
||||
const rowStyle = context?.getRowStyle?.(rowData);
|
||||
const enableAlternatingRowColors =
|
||||
context?.enableAlternatingRowColors ?? false;
|
||||
|
||||
const rowClassName = cx(
|
||||
tableStyles.tableRow,
|
||||
isActive && tableStyles.tableRowActive,
|
||||
enableAlternatingRowColors &&
|
||||
(item.row.index % 2 === 0
|
||||
? tableStyles.tableRowEven
|
||||
: tableStyles.tableRowOdd),
|
||||
extraClass,
|
||||
);
|
||||
|
||||
@@ -105,6 +111,12 @@ function areTableRowPropsEqual<TData>(
|
||||
if (prev.context?.columnVisibilityKey !== next.context?.columnVisibilityKey) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
prev.context?.enableAlternatingRowColors !==
|
||||
next.context?.enableAlternatingRowColors
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prev.context !== next.context) {
|
||||
const prevActive = prev.context?.isRowActive?.(prevData) ?? false;
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
padding: 0.3rem;
|
||||
padding: var(--tanstack-cell-padding-top, 0.3rem)
|
||||
var(--tanstack-cell-padding-right, 0.3rem)
|
||||
var(--tanstack-cell-padding-bottom, 0.3rem)
|
||||
var(--tanstack-cell-padding-left, 0.3rem);
|
||||
transform: translate3d(
|
||||
var(--tanstack-header-translate-x, 0px),
|
||||
var(--tanstack-header-translate-y, 0px),
|
||||
@@ -19,7 +22,17 @@
|
||||
}
|
||||
|
||||
border: none !important;
|
||||
background-color: var(--l2-background) !important;
|
||||
background-color: var(
|
||||
--tanstack-table-header-cell-bg,
|
||||
var(--l2-background)
|
||||
) !important;
|
||||
|
||||
&:first-child {
|
||||
background-color: var(
|
||||
--tanstack-first-column-header-bg,
|
||||
var(--tanstack-table-header-cell-bg, var(--l2-background))
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tanstackHeaderContent {
|
||||
@@ -61,7 +74,7 @@
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
cursor: grab;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--tanstack-table-header-cell-color, var(--l2-foreground));
|
||||
opacity: 1;
|
||||
touch-action: none;
|
||||
}
|
||||
@@ -74,7 +87,7 @@
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--tanstack-table-header-cell-color, var(--l2-foreground));
|
||||
|
||||
margin-left: auto;
|
||||
}
|
||||
@@ -82,8 +95,9 @@
|
||||
.tanstackColumnActionsContent {
|
||||
width: 140px;
|
||||
padding: 0;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--tanstack-table-header-cell-bg, var(--l2-background));
|
||||
border: 1px solid
|
||||
var(--tanstack-table-header-cell-actions-border-color, var(--l2-border));
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -137,7 +151,7 @@
|
||||
}
|
||||
|
||||
.tanstackHeaderCell.isResizing .cursorColResize {
|
||||
background: var(--bg-robin-300);
|
||||
background: var(--tanstack-table-resize-active-bg, var(--bg-robin-300));
|
||||
}
|
||||
|
||||
.tanstackResizeHandleLine {
|
||||
@@ -147,7 +161,7 @@
|
||||
left: 50%;
|
||||
width: 4px;
|
||||
transform: translateX(-50%);
|
||||
background: var(--l2-background);
|
||||
background: var(--tanstack-table-resize-handle-bg, var(--l2-background));
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
transition:
|
||||
@@ -155,13 +169,34 @@
|
||||
width 120ms ease;
|
||||
}
|
||||
|
||||
.tanstackHeaderCell:first-child .tanstackResizeHandleLine {
|
||||
background: var(
|
||||
--tanstack-first-column-header-bg,
|
||||
var(--tanstack-table-resize-handle-bg, var(--l2-background))
|
||||
);
|
||||
}
|
||||
|
||||
.cursorColResize:hover .tanstackResizeHandleLine {
|
||||
background: var(--l2-border);
|
||||
background: var(--tanstack-table-resize-handle-hover-bg, var(--l2-border));
|
||||
}
|
||||
|
||||
.tanstackHeaderCell:first-child
|
||||
.cursorColResize:hover
|
||||
.tanstackResizeHandleLine {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(
|
||||
--tanstack-first-column-header-bg,
|
||||
var(--tanstack-table-resize-handle-bg, var(--l2-background))
|
||||
)
|
||||
60%,
|
||||
black
|
||||
);
|
||||
}
|
||||
|
||||
.tanstackHeaderCell.isResizing .tanstackResizeHandleLine {
|
||||
width: 2px;
|
||||
background: var(--bg-robin-500);
|
||||
background: var(--tanstack-table-resize-handle-active-bg, var(--bg-robin-500));
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -213,7 +248,12 @@
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--l3-foreground);
|
||||
|
||||
&[data-sort-direction='asc'],
|
||||
&[data-sort-direction='desc'] {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.isSortable {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useSortable } from '@dnd-kit/sortable';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui';
|
||||
import { flexRender, Header as TanStackHeader } from '@tanstack/react-table';
|
||||
import cx from 'classnames';
|
||||
import { ChevronDown, ChevronUp, GripVertical } from 'lucide-react';
|
||||
import { ArrowDown, ArrowUp, ArrowUpDown, GripVertical } from 'lucide-react';
|
||||
|
||||
import { SortState, TableColumnDef } from './types';
|
||||
|
||||
@@ -177,12 +177,17 @@ function TanStackHeaderRow<TData>({
|
||||
? column.header()
|
||||
: String(column.header || '').replace(/^\w/, (c) => c.toUpperCase())}
|
||||
</span>
|
||||
<span className={headerStyles.tanstackSortIndicator}>
|
||||
<span
|
||||
className={headerStyles.tanstackSortIndicator}
|
||||
data-sort-direction={currentSortDirection || 'none'}
|
||||
>
|
||||
{currentSortDirection === 'asc' ? (
|
||||
<ChevronUp size={SORT_ICON_SIZE} />
|
||||
<ArrowUp size={SORT_ICON_SIZE} />
|
||||
) : currentSortDirection === 'desc' ? (
|
||||
<ChevronDown size={SORT_ICON_SIZE} />
|
||||
) : null}
|
||||
<ArrowDown size={SORT_ICON_SIZE} />
|
||||
) : (
|
||||
<ArrowUpDown size={SORT_ICON_SIZE} />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
.tanStackTable {
|
||||
--tanstack-cell-padding: var(--tanstack-cell-padding-override, 0.3rem);
|
||||
--tanstack-cell-padding-left: var(
|
||||
--tanstack-cell-padding-left-override,
|
||||
var(--tanstack-cell-padding)
|
||||
);
|
||||
--tanstack-cell-padding-right: var(
|
||||
--tanstack-cell-padding-right-override,
|
||||
var(--tanstack-cell-padding)
|
||||
);
|
||||
--tanstack-cell-padding-top: var(
|
||||
--tanstack-cell-padding-top-override,
|
||||
var(--tanstack-cell-padding)
|
||||
);
|
||||
--tanstack-cell-padding-bottom: var(
|
||||
--tanstack-cell-padding-bottom-override,
|
||||
var(--tanstack-cell-padding)
|
||||
);
|
||||
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
@@ -26,7 +44,7 @@
|
||||
line-clamp: var(--tanstack-plain-body-line-clamp, 1);
|
||||
font-size: var(--tanstack-plain-cell-font-size, 14px);
|
||||
line-height: var(--tanstack-plain-cell-line-height, 18px);
|
||||
color: var(--l2-foreground);
|
||||
color: var(--tanstack-table-cell-color, var(--l2-foreground));
|
||||
max-width: 100%;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -42,13 +60,35 @@
|
||||
}
|
||||
|
||||
.tableCell {
|
||||
padding: 0.3rem;
|
||||
padding: var(--tanstack-cell-padding-top) var(--tanstack-cell-padding-right)
|
||||
var(--tanstack-cell-padding-bottom) var(--tanstack-cell-padding-left);
|
||||
height: var(--tanstack-table-row-height, auto);
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.07px;
|
||||
font-size: var(--tanstack-plain-cell-font-size, 14px);
|
||||
line-height: var(--tanstack-plain-cell-line-height, 18px);
|
||||
color: var(--l2-foreground);
|
||||
color: var(--tanstack-table-cell-color, var(--l2-foreground));
|
||||
background-color: var(--tanstack-table-cell-bg, transparent);
|
||||
|
||||
&:first-child {
|
||||
padding: var(
|
||||
--tanstack-cell-padding-top-first-column,
|
||||
var(--tanstack-cell-padding-top)
|
||||
)
|
||||
var(
|
||||
--tanstack-cell-padding-right-first-column,
|
||||
var(--tanstack-cell-padding-right)
|
||||
)
|
||||
var(
|
||||
--tanstack-cell-padding-bottom-first-column,
|
||||
var(--tanstack-cell-padding-bottom)
|
||||
)
|
||||
var(
|
||||
--tanstack-cell-padding-left-first-column,
|
||||
var(--tanstack-cell-padding-left)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.tableRow {
|
||||
@@ -58,19 +98,69 @@
|
||||
|
||||
&:hover {
|
||||
.tableCell {
|
||||
background-color: var(--row-hover-bg) !important;
|
||||
background-color: var(
|
||||
--tanstack-table-row-hover-bg,
|
||||
var(--row-hover-bg)
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.tableRowActive {
|
||||
.tableCell {
|
||||
background-color: var(--row-active-bg) !important;
|
||||
background-color: var(
|
||||
--tanstack-table-row-active-bg,
|
||||
var(--row-active-bg)
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.tableRowOdd {
|
||||
.tableCell {
|
||||
background-color: var(--tanstack-table-row-odd-bg, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
&.tableRowEven {
|
||||
.tableCell {
|
||||
background-color: var(--tanstack-table-row-even-bg, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.tableCell:first-child {
|
||||
background-color: var(
|
||||
--tanstack-first-column-bg,
|
||||
var(--tanstack-table-cell-bg, transparent)
|
||||
);
|
||||
color: var(
|
||||
--tanstack-first-column-color,
|
||||
var(--tanstack-table-cell-color, var(--l2-foreground))
|
||||
);
|
||||
}
|
||||
|
||||
&.tableRowOdd .tableCell:first-child {
|
||||
background-color: var(
|
||||
--tanstack-first-column-odd-bg,
|
||||
var(
|
||||
--tanstack-first-column-bg,
|
||||
var(--tanstack-table-row-odd-bg, transparent)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
&.tableRowEven .tableCell:first-child {
|
||||
background-color: var(
|
||||
--tanstack-first-column-even-bg,
|
||||
var(
|
||||
--tanstack-first-column-bg,
|
||||
var(--tanstack-table-row-even-bg, transparent)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.tableHeaderCell {
|
||||
padding: 0.3rem;
|
||||
padding: var(--tanstack-cell-padding-top) var(--tanstack-cell-padding-right)
|
||||
var(--tanstack-cell-padding-bottom) var(--tanstack-cell-padding-left);
|
||||
height: 36px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
@@ -78,20 +168,40 @@
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--l1-foreground);
|
||||
|
||||
// TODO: Remove this once background color (l1) is matching the actual background color of the page
|
||||
&[data-dark-mode='true'] {
|
||||
background: #0b0c0d;
|
||||
}
|
||||
|
||||
&[data-dark-mode='false'] {
|
||||
background: #fdfdfd;
|
||||
}
|
||||
color: var(--tanstack-table-header-cell-color, var(--l1-foreground));
|
||||
background: var(--tanstack-table-header-cell-bg, var(--l1-background));
|
||||
border-bottom: 1px solid var(--tanstack-table-header-border-color, transparent);
|
||||
}
|
||||
|
||||
.tableRowExpansion {
|
||||
display: table-row;
|
||||
|
||||
.tableCellExpansion {
|
||||
background-color: var(--tanstack-table-header-cell-bg, var(--l1-background));
|
||||
color: var(--tanstack-table-header-cell-color, var(--l1-foreground));
|
||||
}
|
||||
|
||||
& > td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& thead th:first-child {
|
||||
padding-left: var(
|
||||
--tanstack-expansion-first-col-padding-left,
|
||||
calc(var(--spacing-3) - 1px)
|
||||
);
|
||||
}
|
||||
|
||||
& tbody td:first-child {
|
||||
padding-left: var(
|
||||
--tanstack-expansion-first-col-padding-left,
|
||||
calc(var(--spacing-20) + var(--spacing-4))
|
||||
);
|
||||
}
|
||||
|
||||
:global(thead) {
|
||||
position: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tableCellExpansion {
|
||||
|
||||
@@ -90,6 +90,7 @@ function TanStackTableInner<TData>(
|
||||
skeletonRowCount = 10,
|
||||
enableQueryParams,
|
||||
pagination,
|
||||
paginationClassname,
|
||||
onEndReached,
|
||||
getRowKey,
|
||||
getItemKey,
|
||||
@@ -112,9 +113,17 @@ function TanStackTableInner<TData>(
|
||||
testId,
|
||||
prefixPaginationContent,
|
||||
suffixPaginationContent,
|
||||
enableAlternatingRowColors,
|
||||
disableVirtualScroll,
|
||||
}: TanStackTableProps<TData>,
|
||||
forwardedRef: React.ForwardedRef<TanStackTableHandle>,
|
||||
): JSX.Element {
|
||||
if (disableVirtualScroll && onEndReached) {
|
||||
throw new Error(
|
||||
'TanStackTable: Cannot use onEndReached with disableVirtualScroll. Infinite scroll requires virtualization.',
|
||||
);
|
||||
}
|
||||
|
||||
const virtuosoRef = useRef<TableVirtuosoHandle | null>(null);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@@ -221,6 +230,15 @@ function TanStackTableInner<TData>(
|
||||
[getRowCanExpand],
|
||||
);
|
||||
|
||||
const isExpandEnabled = Boolean(renderExpandedRow);
|
||||
useEffect(() => {
|
||||
const hasExpanded =
|
||||
typeof expanded === 'boolean' ? expanded : Object.keys(expanded).length > 0;
|
||||
if (!isExpandEnabled && hasExpanded) {
|
||||
setExpanded({});
|
||||
}
|
||||
}, [isExpandEnabled, expanded, setExpanded]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: effectiveData,
|
||||
columns: tanstackColumns,
|
||||
@@ -229,7 +247,7 @@ function TanStackTableInner<TData>(
|
||||
columnResizeMode: 'onChange',
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId,
|
||||
enableExpanding: Boolean(renderExpandedRow),
|
||||
enableExpanding: isExpandEnabled,
|
||||
getRowCanExpand: renderExpandedRow ? tableGetRowCanExpand : undefined,
|
||||
onColumnSizingChange: handleColumnSizingChange,
|
||||
onColumnVisibilityChange: noopColumnVisibility,
|
||||
@@ -333,6 +351,7 @@ function TanStackTableInner<TData>(
|
||||
hasSingleColumn,
|
||||
columnOrderKey,
|
||||
columnVisibilityKey,
|
||||
enableAlternatingRowColors,
|
||||
}),
|
||||
[
|
||||
getRowStyle,
|
||||
@@ -350,6 +369,7 @@ function TanStackTableInner<TData>(
|
||||
hasSingleColumn,
|
||||
columnOrderKey,
|
||||
columnVisibilityKey,
|
||||
enableAlternatingRowColors,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -500,7 +520,10 @@ function TanStackTableInner<TData>(
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(viewStyles.tanstackTableViewWrapper, className)}>
|
||||
<div
|
||||
className={cx(viewStyles.tanstackTableViewWrapper, className)}
|
||||
data-has-group-by={(groupBy?.length || 0) > 0}
|
||||
>
|
||||
<TanStackTableStateProvider>
|
||||
<TableLoadingSync
|
||||
isLoading={isLoading}
|
||||
@@ -508,23 +531,53 @@ function TanStackTableInner<TData>(
|
||||
/>
|
||||
<ColumnVisibilitySync visibility={effectiveVisibility} />
|
||||
<TooltipProvider>
|
||||
<TableVirtuoso<FlatItem<TData>, TableRowContext<TData>>
|
||||
className={virtuosoClassName}
|
||||
ref={virtuosoRef}
|
||||
{...restTableScrollerProps}
|
||||
data={flatItems}
|
||||
totalCount={flatItems.length}
|
||||
context={virtuosoContext}
|
||||
increaseViewportBy={INCREASE_VIEWPORT_BY}
|
||||
initialTopMostItemIndex={
|
||||
flatIndexForActiveRow >= 0 ? flatIndexForActiveRow : 0
|
||||
}
|
||||
fixedHeaderContent={tableHeader}
|
||||
style={virtuosoTableStyle}
|
||||
components={virtuosoComponents}
|
||||
endReached={onEndReached ? handleEndReached : undefined}
|
||||
data-testid={testId}
|
||||
/>
|
||||
{disableVirtualScroll ? (
|
||||
<div
|
||||
className={virtuosoClassName}
|
||||
{...restTableScrollerProps}
|
||||
data-testid={testId}
|
||||
>
|
||||
<table className={tableStyles.tanStackTable} style={virtuosoTableStyle}>
|
||||
<VirtuosoTableColGroup columns={effectiveColumns} table={table} />
|
||||
<thead>{tableHeader()}</thead>
|
||||
<tbody>
|
||||
{(isLoading && data.length === 0
|
||||
? flatItems.slice(0, skeletonRowCount)
|
||||
: flatItems
|
||||
).map((item, index) => (
|
||||
<TanStackCustomTableRow
|
||||
key={
|
||||
item.kind === 'expansion' ? `${item.row.id}-expansion` : item.row.id
|
||||
}
|
||||
item={item}
|
||||
context={virtuosoContext}
|
||||
data-index={index}
|
||||
data-item-index={index}
|
||||
data-known-size={0}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<TableVirtuoso<FlatItem<TData>, TableRowContext<TData>>
|
||||
className={virtuosoClassName}
|
||||
ref={virtuosoRef}
|
||||
{...restTableScrollerProps}
|
||||
data={flatItems}
|
||||
totalCount={flatItems.length}
|
||||
context={virtuosoContext}
|
||||
increaseViewportBy={INCREASE_VIEWPORT_BY}
|
||||
initialTopMostItemIndex={
|
||||
flatIndexForActiveRow >= 0 ? flatIndexForActiveRow : 0
|
||||
}
|
||||
fixedHeaderContent={tableHeader}
|
||||
style={virtuosoTableStyle}
|
||||
components={virtuosoComponents}
|
||||
endReached={onEndReached ? handleEndReached : undefined}
|
||||
data-testid={testId}
|
||||
/>
|
||||
)}
|
||||
{showInfiniteScrollLoader && (
|
||||
<div
|
||||
className={viewStyles.tanstackLoadingOverlay}
|
||||
@@ -534,7 +587,7 @@ function TanStackTableInner<TData>(
|
||||
</div>
|
||||
)}
|
||||
{showPagination && pagination && (
|
||||
<div className={viewStyles.paginationContainer}>
|
||||
<div className={cx(viewStyles.paginationContainer, paginationClassname)}>
|
||||
{prefixPaginationContent}
|
||||
<Pagination
|
||||
current={page}
|
||||
|
||||
@@ -49,7 +49,8 @@
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--bg-slate-300) transparent;
|
||||
scrollbar-color: var(--tanstack-table-scrollbar-color, var(--bg-slate-300))
|
||||
transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
@@ -65,12 +66,12 @@
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-slate-300);
|
||||
background: var(--tanstack-table-scrollbar-color, var(--bg-slate-300));
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bg-slate-200);
|
||||
background: var(--tanstack-table-scrollbar-hover-color, var(--bg-slate-200));
|
||||
}
|
||||
|
||||
&.cellTypographySmall {
|
||||
@@ -135,18 +136,25 @@
|
||||
z-index: 3;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
background: var(--l1-background);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
background: var(--tanstack-table-loading-overlay-bg, var(--l1-background));
|
||||
box-shadow: var(
|
||||
--tanstack-table-loading-overlay-shadow,
|
||||
0 2px 8px rgba(0, 0, 0, 0.15)
|
||||
);
|
||||
}
|
||||
|
||||
:global(.lightMode) .tanstackTableVirtuosoScroll {
|
||||
scrollbar-color: var(--bg-vanilla-300) transparent;
|
||||
scrollbar-color: var(--tanstack-table-scrollbar-color, var(--bg-vanilla-300))
|
||||
transparent;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-vanilla-300);
|
||||
background: var(--tanstack-table-scrollbar-color, var(--bg-vanilla-300));
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bg-vanilla-100);
|
||||
background: var(
|
||||
--tanstack-table-scrollbar-hover-color,
|
||||
var(--bg-vanilla-100)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,6 +389,101 @@ describe('TanStackTableView Integration', () => {
|
||||
// by checking the table renders without errors
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders without errors when expanded state exists but expansion is disabled', async () => {
|
||||
// This tests that the table handles the case where URL has expanded state
|
||||
// but renderExpandedRow is undefined (expansion disabled).
|
||||
// The table's useEffect should reset expanded state automatically.
|
||||
renderTanStackTable({
|
||||
props: {
|
||||
enableQueryParams: true,
|
||||
// renderExpandedRow is undefined - expansion disabled
|
||||
},
|
||||
queryParams: { expanded: '["1"]' },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Table should render without any expanded rows
|
||||
expect(screen.queryByTestId('expanded-content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders expanded rows with unique keys in non-virtualized mode', async () => {
|
||||
// This tests that row and expansion items have unique keys to avoid
|
||||
// React's "duplicate key" warning when disableVirtualScroll is true
|
||||
renderTanStackTable({
|
||||
props: {
|
||||
disableVirtualScroll: true,
|
||||
enableQueryParams: true,
|
||||
renderExpandedRow: (row) => (
|
||||
<div data-testid={`expanded-${row.id}`}>Expanded: {row.name}</div>
|
||||
),
|
||||
getRowCanExpand: () => true,
|
||||
getRowKey: (row) => row.id,
|
||||
},
|
||||
queryParams: { expanded: '["1"]' },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Both the row and its expansion content should be rendered
|
||||
expect(screen.getByTestId('expanded-1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Expanded: Item 1')).toBeInTheDocument();
|
||||
|
||||
// Verify all 3 data rows plus 1 expansion row = 4 tr elements in tbody
|
||||
const tbody = screen.getByRole('table').querySelector('tbody');
|
||||
expect(tbody?.querySelectorAll('tr')).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disableVirtualScroll', () => {
|
||||
it('throws error when used with onEndReached', () => {
|
||||
expect(() => {
|
||||
renderTanStackTable({
|
||||
props: {
|
||||
disableVirtualScroll: true,
|
||||
onEndReached: jest.fn(),
|
||||
},
|
||||
});
|
||||
}).toThrow(
|
||||
'TanStackTable: Cannot use onEndReached with disableVirtualScroll. Infinite scroll requires virtualization.',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders all rows without virtualization', async () => {
|
||||
renderTanStackTable({
|
||||
props: {
|
||||
disableVirtualScroll: true,
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Item 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Item 3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify table structure exists
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders column headers without virtualization', async () => {
|
||||
renderTanStackTable({
|
||||
props: {
|
||||
disableVirtualScroll: true,
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('ID')).toBeInTheDocument();
|
||||
expect(screen.getByText('Name')).toBeInTheDocument();
|
||||
expect(screen.getByText('Value')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('infinite scroll', () => {
|
||||
|
||||
@@ -138,7 +138,10 @@ describe('useTableParams (URL mode — enableQueryParams set)', () => {
|
||||
|
||||
it('reads initial page from URL params', () => {
|
||||
const wrapper = createNuqsWrapper({ page: '3' });
|
||||
const { result } = renderHook(() => useTableParams(true), { wrapper });
|
||||
// Pass matching default to prevent reset on mount (page resets when orderBy changes)
|
||||
const { result } = renderHook(() => useTableParams(true, { page: 3 }), {
|
||||
wrapper,
|
||||
});
|
||||
expect(result.current.page).toBe(3);
|
||||
});
|
||||
|
||||
@@ -249,3 +252,294 @@ describe('useTableParams (URL mode — enableQueryParams set)', () => {
|
||||
expect(result.current.orderBy).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useTableParams (selective URL mode — partial config object)', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('syncs only page to URL when only page is configured', () => {
|
||||
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
|
||||
const wrapper = createNuqsWrapper({}, onUrlUpdate);
|
||||
const { result } = renderHook(() => useTableParams({ page: 'myPage' }), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Update page - should sync to URL
|
||||
act(() => {
|
||||
result.current.setPage(5);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(result.current.page).toBe(5);
|
||||
const lastPage = onUrlUpdate.mock.calls
|
||||
.map((call) => call[0].searchParams.get('myPage'))
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
expect(lastPage).toBe('5');
|
||||
|
||||
// Update limit - should stay local (not in URL)
|
||||
act(() => {
|
||||
result.current.setLimit(100);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(result.current.limit).toBe(100);
|
||||
const limitInUrl = onUrlUpdate.mock.calls.some(
|
||||
(call) => call[0].searchParams.get('limit') !== null,
|
||||
);
|
||||
expect(limitInUrl).toBe(false);
|
||||
|
||||
// Update orderBy - should stay local (not in URL)
|
||||
act(() => {
|
||||
result.current.setOrderBy({ columnName: 'test', order: 'asc' });
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(result.current.orderBy).toStrictEqual({
|
||||
columnName: 'test',
|
||||
order: 'asc',
|
||||
});
|
||||
const orderByInUrl = onUrlUpdate.mock.calls.some(
|
||||
(call) => call[0].searchParams.get('order_by') !== null,
|
||||
);
|
||||
expect(orderByInUrl).toBe(false);
|
||||
});
|
||||
|
||||
it('syncs only orderBy to URL when only orderBy is configured', () => {
|
||||
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
|
||||
const wrapper = createNuqsWrapper({}, onUrlUpdate);
|
||||
const { result } = renderHook(() => useTableParams({ orderBy: 'mySort' }), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Update orderBy - should sync to URL
|
||||
act(() => {
|
||||
result.current.setOrderBy({ columnName: 'cpu', order: 'desc' });
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(result.current.orderBy).toStrictEqual({
|
||||
columnName: 'cpu',
|
||||
order: 'desc',
|
||||
});
|
||||
const lastOrderBy = onUrlUpdate.mock.calls
|
||||
.map((call) => call[0].searchParams.get('mySort'))
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
expect(lastOrderBy).toBeDefined();
|
||||
expect(JSON.parse(lastOrderBy!)).toStrictEqual({
|
||||
columnName: 'cpu',
|
||||
order: 'desc',
|
||||
});
|
||||
|
||||
// Update page - should stay local
|
||||
act(() => {
|
||||
result.current.setPage(3);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(result.current.page).toBe(3);
|
||||
const pageInUrl = onUrlUpdate.mock.calls.some(
|
||||
(call) => call[0].searchParams.get('page') !== null,
|
||||
);
|
||||
expect(pageInUrl).toBe(false);
|
||||
});
|
||||
|
||||
it('syncs only limit to URL when only limit is configured', () => {
|
||||
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
|
||||
const wrapper = createNuqsWrapper({}, onUrlUpdate);
|
||||
const { result } = renderHook(() => useTableParams({ limit: 'myLimit' }), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Update limit - should sync to URL
|
||||
act(() => {
|
||||
result.current.setLimit(25);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(result.current.limit).toBe(25);
|
||||
const lastLimit = onUrlUpdate.mock.calls
|
||||
.map((call) => call[0].searchParams.get('myLimit'))
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
expect(lastLimit).toBe('25');
|
||||
|
||||
// Update page - should stay local
|
||||
act(() => {
|
||||
result.current.setPage(2);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(result.current.page).toBe(2);
|
||||
const pageInUrl = onUrlUpdate.mock.calls.some(
|
||||
(call) => call[0].searchParams.get('page') !== null,
|
||||
);
|
||||
expect(pageInUrl).toBe(false);
|
||||
});
|
||||
|
||||
it('syncs only expanded to URL when only expanded is configured', () => {
|
||||
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
|
||||
const wrapper = createNuqsWrapper({}, onUrlUpdate);
|
||||
const { result } = renderHook(
|
||||
() => useTableParams({ expanded: 'myExpanded' }),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Update expanded - should sync to URL
|
||||
act(() => {
|
||||
result.current.setExpanded({ 'row-1': true, 'row-2': true });
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(result.current.expanded).toStrictEqual({
|
||||
'row-1': true,
|
||||
'row-2': true,
|
||||
});
|
||||
const lastExpanded = onUrlUpdate.mock.calls
|
||||
.map((call) => call[0].searchParams.get('myExpanded'))
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
expect(lastExpanded).toBeDefined();
|
||||
expect(JSON.parse(lastExpanded!)).toEqual(
|
||||
expect.arrayContaining(['row-1', 'row-2']),
|
||||
);
|
||||
|
||||
// Update page - should stay local
|
||||
act(() => {
|
||||
result.current.setPage(4);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(result.current.page).toBe(4);
|
||||
const pageInUrl = onUrlUpdate.mock.calls.some(
|
||||
(call) => call[0].searchParams.get('page') !== null,
|
||||
);
|
||||
expect(pageInUrl).toBe(false);
|
||||
});
|
||||
|
||||
it('syncs page and orderBy to URL but keeps limit and expanded local', () => {
|
||||
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
|
||||
const wrapper = createNuqsWrapper({}, onUrlUpdate);
|
||||
const { result } = renderHook(
|
||||
() => useTableParams({ page: 'p', orderBy: 'sort' }),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Update limit and expanded first (should stay local)
|
||||
act(() => {
|
||||
result.current.setLimit(75);
|
||||
result.current.setExpanded({ 'row-5': true });
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(result.current.limit).toBe(75);
|
||||
expect(result.current.expanded).toStrictEqual({ 'row-5': true });
|
||||
|
||||
// Update page (should sync to URL)
|
||||
act(() => {
|
||||
result.current.setPage(2);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(result.current.page).toBe(2);
|
||||
const lastPage = onUrlUpdate.mock.calls
|
||||
.map((call) => call[0].searchParams.get('p'))
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
expect(lastPage).toBe('2');
|
||||
|
||||
// Update orderBy (should sync to URL, and resets page to default)
|
||||
act(() => {
|
||||
result.current.setOrderBy({ columnName: 'name', order: 'asc' });
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(result.current.orderBy).toStrictEqual({
|
||||
columnName: 'name',
|
||||
order: 'asc',
|
||||
});
|
||||
const lastOrderBy = onUrlUpdate.mock.calls
|
||||
.map((call) => call[0].searchParams.get('sort'))
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
expect(lastOrderBy).toBeDefined();
|
||||
|
||||
// limit should NOT be in URL
|
||||
const limitInUrl = onUrlUpdate.mock.calls.some(
|
||||
(call) =>
|
||||
call[0].searchParams.get('limit') !== null ||
|
||||
call[0].searchParams.get('myLimit') !== null,
|
||||
);
|
||||
expect(limitInUrl).toBe(false);
|
||||
|
||||
// expanded should NOT be in URL
|
||||
const expandedInUrl = onUrlUpdate.mock.calls.some(
|
||||
(call) =>
|
||||
call[0].searchParams.get('expanded') !== null ||
|
||||
call[0].searchParams.get('myExpanded') !== null,
|
||||
);
|
||||
expect(expandedInUrl).toBe(false);
|
||||
});
|
||||
|
||||
it('reads initial values from URL for configured params only', () => {
|
||||
const wrapper = createNuqsWrapper({
|
||||
customPage: '7',
|
||||
limit: '999', // This should be ignored since limit is not configured
|
||||
});
|
||||
const { result } = renderHook(
|
||||
// Pass page default matching URL to prevent reset on mount
|
||||
() => useTableParams({ page: 'customPage' }, { page: 7 }),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Page should come from URL
|
||||
expect(result.current.page).toBe(7);
|
||||
// Limit should be default (not from URL since it's not configured)
|
||||
expect(result.current.limit).toBe(50);
|
||||
});
|
||||
|
||||
it('supports updater function for expanded state', () => {
|
||||
const wrapper = createNuqsWrapper();
|
||||
const { result } = renderHook(() => useTableParams({ expanded: 'exp' }), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Set initial expanded state
|
||||
act(() => {
|
||||
result.current.setExpanded({ 'row-1': true });
|
||||
});
|
||||
expect(result.current.expanded).toStrictEqual({ 'row-1': true });
|
||||
|
||||
// Use updater function to add another row
|
||||
act(() => {
|
||||
result.current.setExpanded((prev) => ({
|
||||
...(typeof prev === 'boolean' ? {} : prev),
|
||||
'row-2': true,
|
||||
}));
|
||||
});
|
||||
expect(result.current.expanded).toStrictEqual({
|
||||
'row-1': true,
|
||||
'row-2': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('supports updater function for local expanded state', () => {
|
||||
const wrapper = createNuqsWrapper();
|
||||
const { result } = renderHook(() => useTableParams(), { wrapper });
|
||||
|
||||
// Set initial expanded state
|
||||
act(() => {
|
||||
result.current.setExpanded({ 'row-a': true });
|
||||
});
|
||||
expect(result.current.expanded).toStrictEqual({ 'row-a': true });
|
||||
|
||||
// Use updater function
|
||||
act(() => {
|
||||
result.current.setExpanded((prev) => ({
|
||||
...(typeof prev === 'boolean' ? {} : prev),
|
||||
'row-b': true,
|
||||
}));
|
||||
});
|
||||
expect(result.current.expanded).toStrictEqual({
|
||||
'row-a': true,
|
||||
'row-b': true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -171,6 +171,27 @@ export * from './useTableParams';
|
||||
* tableScrollerProps={{ className: 'my-table-scroll', 'data-testid': 'logs-scroller' }}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @example Disable virtual scroll — useful for nested tables inside expanded rows.
|
||||
* Virtual scroll requires a fixed height container, which is problematic for nested tables
|
||||
* that need dynamic height. Use `disableVirtualScroll` when rendering tables inside
|
||||
* `renderExpandedRow` to allow the nested table to grow based on content.
|
||||
* Note: Cannot be combined with `onEndReached` (infinite scroll requires virtualization).
|
||||
* ```tsx
|
||||
* // Parent table with expandable rows
|
||||
* <TanStackTable
|
||||
* data={parentData}
|
||||
* columns={parentColumns}
|
||||
* renderExpandedRow={(row) => (
|
||||
* // Nested table without virtualization — height adapts to content
|
||||
* <TanStackTable
|
||||
* data={row.children}
|
||||
* columns={childColumns}
|
||||
* disableVirtualScroll
|
||||
* />
|
||||
* )}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
const TanStackTable = Object.assign(TanStackTableBase, {
|
||||
Text: TanStackTableText,
|
||||
|
||||
@@ -107,6 +107,8 @@ export type TableRowContext<TData> = {
|
||||
columnOrderKey: string;
|
||||
/** Column visibility key for memo invalidation on visibility change */
|
||||
columnVisibilityKey: string;
|
||||
/** Enable alternating row background colors (zebra striping) */
|
||||
enableAlternatingRowColors?: boolean;
|
||||
};
|
||||
|
||||
export type PaginationProps = {
|
||||
@@ -116,10 +118,10 @@ export type PaginationProps = {
|
||||
};
|
||||
|
||||
export type TanstackTableQueryParamsConfig = {
|
||||
page: string;
|
||||
limit: string;
|
||||
orderBy: string;
|
||||
expanded: string;
|
||||
page?: string;
|
||||
limit?: string;
|
||||
orderBy?: string;
|
||||
expanded?: string;
|
||||
};
|
||||
|
||||
export type TanStackTableProps<TData> = {
|
||||
@@ -137,6 +139,7 @@ export type TanStackTableProps<TData> = {
|
||||
skeletonRowCount?: number;
|
||||
enableQueryParams?: boolean | string | TanstackTableQueryParamsConfig;
|
||||
pagination?: PaginationProps;
|
||||
paginationClassname?: string;
|
||||
onEndReached?: (index: number) => void;
|
||||
/** Function to get the unique key for a row (before duplicate handling).
|
||||
* When set, enables automatic duplicate key detection and group-aware key composition. */
|
||||
@@ -176,6 +179,10 @@ export type TanStackTableProps<TData> = {
|
||||
prefixPaginationContent?: ReactNode;
|
||||
/** Content rendered after the pagination controls */
|
||||
suffixPaginationContent?: ReactNode;
|
||||
/** Enable alternating row background colors (zebra striping) */
|
||||
enableAlternatingRowColors?: boolean;
|
||||
/** Disable virtual scrolling and render all rows at once. Cannot be used with onEndReached. */
|
||||
disableVirtualScroll?: boolean;
|
||||
};
|
||||
|
||||
export type TanStackTableHandle = TableVirtuosoHandle & {
|
||||
|
||||
@@ -8,6 +8,12 @@ import { SortState, TanstackTableQueryParamsConfig } from './types';
|
||||
const NUQS_OPTIONS = { history: 'push' as const };
|
||||
const DEFAULT_PAGE = 1;
|
||||
const DEFAULT_LIMIT = 50;
|
||||
const URL_KEYS_DEFAULT = {
|
||||
page: 'page',
|
||||
limit: 'limit',
|
||||
orderBy: 'order_by',
|
||||
expanded: 'expanded',
|
||||
} as const;
|
||||
|
||||
type Defaults = {
|
||||
page?: number;
|
||||
@@ -49,30 +55,49 @@ export function useTableParams(
|
||||
enableQueryParams?: boolean | string | TanstackTableQueryParamsConfig,
|
||||
defaults?: Defaults,
|
||||
): TableParamsResult {
|
||||
// Determine which params should sync to URL vs use local state
|
||||
const isObjectConfig = typeof enableQueryParams === 'object';
|
||||
const useUrlForPage =
|
||||
enableQueryParams === true ||
|
||||
typeof enableQueryParams === 'string' ||
|
||||
(isObjectConfig && enableQueryParams.page !== undefined);
|
||||
const useUrlForLimit =
|
||||
enableQueryParams === true ||
|
||||
typeof enableQueryParams === 'string' ||
|
||||
(isObjectConfig && enableQueryParams.limit !== undefined);
|
||||
const useUrlForOrderBy =
|
||||
enableQueryParams === true ||
|
||||
typeof enableQueryParams === 'string' ||
|
||||
(isObjectConfig && enableQueryParams.orderBy !== undefined);
|
||||
const useUrlForExpanded =
|
||||
enableQueryParams === true ||
|
||||
typeof enableQueryParams === 'string' ||
|
||||
(isObjectConfig && enableQueryParams.expanded !== undefined);
|
||||
|
||||
const pageQueryParam =
|
||||
typeof enableQueryParams === 'string'
|
||||
? `${enableQueryParams}_page`
|
||||
: typeof enableQueryParams === 'object'
|
||||
? enableQueryParams.page
|
||||
: 'page';
|
||||
? `${enableQueryParams}_${URL_KEYS_DEFAULT.page}`
|
||||
: isObjectConfig
|
||||
? (enableQueryParams.page ?? URL_KEYS_DEFAULT.page)
|
||||
: URL_KEYS_DEFAULT.page;
|
||||
const limitQueryParam =
|
||||
typeof enableQueryParams === 'string'
|
||||
? `${enableQueryParams}_limit`
|
||||
: typeof enableQueryParams === 'object'
|
||||
? enableQueryParams.limit
|
||||
: 'limit';
|
||||
? `${enableQueryParams}_${URL_KEYS_DEFAULT.limit}`
|
||||
: isObjectConfig
|
||||
? (enableQueryParams.limit ?? URL_KEYS_DEFAULT.limit)
|
||||
: URL_KEYS_DEFAULT.limit;
|
||||
const orderByQueryParam =
|
||||
typeof enableQueryParams === 'string'
|
||||
? `${enableQueryParams}_order_by`
|
||||
: typeof enableQueryParams === 'object'
|
||||
? enableQueryParams.orderBy
|
||||
: 'order_by';
|
||||
? `${enableQueryParams}_${URL_KEYS_DEFAULT.orderBy}`
|
||||
: isObjectConfig
|
||||
? (enableQueryParams.orderBy ?? URL_KEYS_DEFAULT.orderBy)
|
||||
: URL_KEYS_DEFAULT.orderBy;
|
||||
const expandedQueryParam =
|
||||
typeof enableQueryParams === 'string'
|
||||
? `${enableQueryParams}_expanded`
|
||||
: typeof enableQueryParams === 'object'
|
||||
? enableQueryParams.expanded
|
||||
: 'expanded';
|
||||
? `${enableQueryParams}_${URL_KEYS_DEFAULT.expanded}`
|
||||
: isObjectConfig
|
||||
? (enableQueryParams.expanded ?? URL_KEYS_DEFAULT.expanded)
|
||||
: URL_KEYS_DEFAULT.expanded;
|
||||
const pageDefault = defaults?.page ?? DEFAULT_PAGE;
|
||||
const limitDefault = defaults?.limit ?? DEFAULT_LIMIT;
|
||||
const orderByDefault = defaults?.orderBy ?? null;
|
||||
@@ -149,45 +174,29 @@ export function useTableParams(
|
||||
|
||||
const orderByDefaultMemoKey = `${orderByDefault?.columnName}${orderByDefault?.order}`;
|
||||
const orderByUrlMemoKey = `${urlOrderBy?.columnName}${urlOrderBy?.order}`;
|
||||
const isEnabledQueryParams =
|
||||
typeof enableQueryParams === 'string' ||
|
||||
typeof enableQueryParams === 'object';
|
||||
|
||||
useEffect(() => {
|
||||
if (isEnabledQueryParams) {
|
||||
if (useUrlForPage) {
|
||||
setUrlPage(pageDefault);
|
||||
} else {
|
||||
setLocalPage(pageDefault);
|
||||
}
|
||||
}, [
|
||||
isEnabledQueryParams,
|
||||
useUrlForPage,
|
||||
orderByDefaultMemoKey,
|
||||
orderByUrlMemoKey,
|
||||
pageDefault,
|
||||
setUrlPage,
|
||||
]);
|
||||
|
||||
if (enableQueryParams) {
|
||||
return {
|
||||
page: urlPage,
|
||||
limit: urlLimit,
|
||||
orderBy: urlOrderBy as SortState | null,
|
||||
expanded: urlExpanded,
|
||||
setPage: setUrlPage,
|
||||
setLimit: setUrlLimit,
|
||||
setOrderBy: setUrlOrderBy,
|
||||
setExpanded: setUrlExpanded,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
page: localPage,
|
||||
limit: localLimit,
|
||||
orderBy: localOrderBy,
|
||||
expanded: localExpanded,
|
||||
setPage: setLocalPage,
|
||||
setLimit: setLocalLimit,
|
||||
setOrderBy: setLocalOrderBy,
|
||||
setExpanded: handleSetLocalExpanded,
|
||||
page: useUrlForPage ? urlPage : localPage,
|
||||
limit: useUrlForLimit ? urlLimit : localLimit,
|
||||
orderBy: (useUrlForOrderBy ? urlOrderBy : localOrderBy) as SortState | null,
|
||||
expanded: useUrlForExpanded ? urlExpanded : localExpanded,
|
||||
setPage: useUrlForPage ? setUrlPage : setLocalPage,
|
||||
setLimit: useUrlForLimit ? setUrlLimit : setLocalLimit,
|
||||
setOrderBy: useUrlForOrderBy ? setUrlOrderBy : setLocalOrderBy,
|
||||
setExpanded: useUrlForExpanded ? setUrlExpanded : handleSetLocalExpanded,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,7 +35,10 @@ export const getColumnWidthStyle = <TData>(
|
||||
): CSSProperties => {
|
||||
// Last column always fills remaining space
|
||||
if (isLastColumn) {
|
||||
return { width: '100%' };
|
||||
return {
|
||||
width: '100%',
|
||||
minWidth: persistedWidth ?? column?.width?.min,
|
||||
};
|
||||
}
|
||||
|
||||
const { width } = column;
|
||||
@@ -59,10 +62,19 @@ export const getColumnWidthStyle = <TData>(
|
||||
};
|
||||
};
|
||||
|
||||
const isSkeletonRow = (row: unknown): boolean => {
|
||||
const r = row as Record<string, unknown>;
|
||||
return typeof r?.id === 'string' && r.id.startsWith('skeleton-');
|
||||
};
|
||||
|
||||
const buildAccessorFn = <TData>(
|
||||
colDef: TableColumnDef<TData>,
|
||||
): ((row: TData) => unknown) => {
|
||||
return (row: TData): unknown => {
|
||||
// Skip accessor for skeleton rows to avoid errors with missing properties
|
||||
if (isSkeletonRow(row)) {
|
||||
return undefined;
|
||||
}
|
||||
if (colDef.accessorFn) {
|
||||
return colDef.accessorFn(row);
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import { K8sBaseList } from 'container/InfraMonitoringK8s/Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from 'container/InfraMonitoringK8s/Base/types';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringFilters,
|
||||
useInfraMonitoringFiltersK8s,
|
||||
useInfraMonitoringPageListing,
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
@@ -32,9 +32,9 @@ import {
|
||||
hostWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
hostColumns,
|
||||
getHostItemKey,
|
||||
getHostRowKey,
|
||||
hostColumnsConfig,
|
||||
hostRenderRowData,
|
||||
} from './table.config';
|
||||
import { getHostsQuickFiltersConfig } from './utils';
|
||||
|
||||
@@ -42,8 +42,8 @@ import styles from './InfraMonitoringHosts.module.scss';
|
||||
|
||||
function Hosts(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
const [, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFilters();
|
||||
const [, setCurrentPage] = useInfraMonitoringPageListing();
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
@@ -170,10 +170,10 @@ function Hosts(): JSX.Element {
|
||||
<K8sBaseList
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
tableColumnsDefinitions={hostColumns}
|
||||
tableColumns={hostColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={hostRenderRowData}
|
||||
getRowKey={getHostRowKey}
|
||||
getItemKey={getHostItemKey}
|
||||
eventCategory={InfraMonitoringEvents.HostEntity}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import * as getHostListsApi from 'api/infraMonitoring/getHostLists';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import * as useQueryBuilderOperations from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import * as timezoneHooks from 'providers/Timezone';
|
||||
import store from 'store';
|
||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
|
||||
import Hosts from '../Hosts';
|
||||
|
||||
jest.mock('lib/getMinMax', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => ({
|
||||
minTime: 1713734400000,
|
||||
maxTime: 1713738000000,
|
||||
isValidShortHandDateTimeFormat: jest.fn().mockReturnValue(true),
|
||||
})),
|
||||
getMinMaxForSelectedTime: jest.fn().mockReturnValue({
|
||||
minTime: 1713734400000000000,
|
||||
maxTime: 1713738000000000000,
|
||||
}),
|
||||
}));
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="date-time-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: ({ onSelect, selectedTime, selectedValue }: any): JSX.Element => (
|
||||
<div data-testid="custom-time-picker">
|
||||
<button onClick={(): void => onSelect('custom')}>
|
||||
{selectedTime} - {selectedValue}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const ROUTES = jest.requireActual('constants/routes').default;
|
||||
return {
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn().mockReturnValue({
|
||||
pathname: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
||||
}),
|
||||
};
|
||||
});
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): any => ({
|
||||
safeNavigate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.spyOn(timezoneHooks, 'useTimezone').mockReturnValue({
|
||||
timezone: {
|
||||
offset: 0,
|
||||
},
|
||||
browserTimezone: {
|
||||
offset: 0,
|
||||
},
|
||||
} as any);
|
||||
|
||||
jest.spyOn(getHostListsApi, 'getHostLists').mockResolvedValue({
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: {
|
||||
status: 'success',
|
||||
data: {
|
||||
type: 'list',
|
||||
records: [
|
||||
{
|
||||
hostName: 'test-host',
|
||||
active: true,
|
||||
os: 'linux',
|
||||
cpu: 0.75,
|
||||
cpuTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
memory: 0.65,
|
||||
memoryTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
wait: 0.03,
|
||||
waitTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
load15: 0.5,
|
||||
load15TimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
},
|
||||
],
|
||||
groups: null,
|
||||
total: 1,
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
endTimeBeforeRetention: false,
|
||||
},
|
||||
},
|
||||
params: {} as any,
|
||||
});
|
||||
|
||||
jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
|
||||
user: {
|
||||
role: 'admin',
|
||||
},
|
||||
featureFlags: [],
|
||||
activeLicenseV3: {
|
||||
event_queue: {
|
||||
created_at: '0',
|
||||
event: LicenseEvent.NO_EVENT,
|
||||
scheduled_at: '0',
|
||||
status: '',
|
||||
updated_at: '0',
|
||||
},
|
||||
license: {
|
||||
license_key: 'test-license-key',
|
||||
license_type: 'trial',
|
||||
org_id: 'test-org-id',
|
||||
plan_id: 'test-plan-id',
|
||||
plan_name: 'test-plan-name',
|
||||
plan_type: 'trial',
|
||||
plan_version: 'test-plan-version',
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
|
||||
currentQuery: initialQueriesMap.metrics,
|
||||
setSupersetQuery: jest.fn(),
|
||||
setLastUsedQuery: jest.fn(),
|
||||
handleSetConfig: jest.fn(),
|
||||
resetQuery: jest.fn(),
|
||||
updateAllQueriesOperators: jest.fn(),
|
||||
} as any);
|
||||
|
||||
jest.spyOn(useQueryBuilderOperations, 'useQueryOperations').mockReturnValue({
|
||||
handleChangeQueryData: jest.fn(),
|
||||
} as any);
|
||||
|
||||
const Wrapper = withNuqsTestingAdapter({ searchParams: {} });
|
||||
|
||||
describe('Hosts', () => {
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
it('renders hosts list table', async () => {
|
||||
const { container } = render(
|
||||
<Wrapper>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<Hosts />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.ant-table')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders filters', async () => {
|
||||
const { container } = render(
|
||||
<Wrapper>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<Hosts />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.filters')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { HostData, TimeSeries } from 'api/infraMonitoring/getHostLists';
|
||||
|
||||
import { hostRenderRowData } from '../table.config';
|
||||
import { getHostsQuickFiltersConfig, HostnameCell } from '../utils';
|
||||
|
||||
const emptyTimeSeries: TimeSeries = {
|
||||
labels: {},
|
||||
labelsArray: [],
|
||||
values: [],
|
||||
};
|
||||
|
||||
describe('InfraMonitoringHosts utils', () => {
|
||||
describe('hostRenderRowData', () => {
|
||||
it('should format host data correctly', () => {
|
||||
const host: HostData = {
|
||||
hostName: 'test-host',
|
||||
active: true,
|
||||
cpu: 0.95,
|
||||
memory: 0.85,
|
||||
wait: 0.05,
|
||||
load15: 2.5,
|
||||
os: 'linux',
|
||||
cpuTimeSeries: emptyTimeSeries,
|
||||
memoryTimeSeries: emptyTimeSeries,
|
||||
waitTimeSeries: emptyTimeSeries,
|
||||
load15TimeSeries: emptyTimeSeries,
|
||||
};
|
||||
|
||||
const result = hostRenderRowData(host, []);
|
||||
|
||||
expect(result.wait).toBe('5%');
|
||||
expect(result.load15).toBe(2.5);
|
||||
expect(result.itemKey).toBe('test-host');
|
||||
expect(result.hostName).toBe('test-host');
|
||||
|
||||
const activeTag = render(result.active as JSX.Element);
|
||||
expect(activeTag.container.textContent).toBe('ACTIVE');
|
||||
expect(activeTag.getByText('ACTIVE')).toBeTruthy();
|
||||
|
||||
const cpuProgress = render(result.cpu as JSX.Element);
|
||||
expect(cpuProgress.container.querySelector('.ant-progress')).toBeTruthy();
|
||||
|
||||
const memoryProgress = render(result.memory as JSX.Element);
|
||||
expect(memoryProgress.container.querySelector('.ant-progress')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle inactive hosts', () => {
|
||||
const host: HostData = {
|
||||
hostName: 'test-host',
|
||||
active: false,
|
||||
cpu: 0.3,
|
||||
memory: 0.4,
|
||||
wait: 0.02,
|
||||
load15: 1.2,
|
||||
os: 'linux',
|
||||
cpuTimeSeries: emptyTimeSeries,
|
||||
memoryTimeSeries: emptyTimeSeries,
|
||||
waitTimeSeries: emptyTimeSeries,
|
||||
load15TimeSeries: emptyTimeSeries,
|
||||
};
|
||||
|
||||
const result = hostRenderRowData(host, []);
|
||||
|
||||
const inactiveTag = render(result.active as JSX.Element);
|
||||
expect(inactiveTag.container.textContent).toBe('INACTIVE');
|
||||
expect(inactiveTag.getByText('INACTIVE')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should use empty itemKey when host has no hostname', () => {
|
||||
const host: 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 = hostRenderRowData(host, []);
|
||||
expect(result.itemKey).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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', () => {
|
||||
it('should return correct config when dotMetricsEnabled is true', () => {
|
||||
const result = getHostsQuickFiltersConfig(true);
|
||||
|
||||
expect(result[0].attributeKey.key).toBe('host.name');
|
||||
expect(result[1].attributeKey.key).toBe('os.type');
|
||||
expect(result[0].aggregateAttribute).toBe('system.cpu.load_average.15m');
|
||||
});
|
||||
|
||||
it('should return correct config when dotMetricsEnabled is false', () => {
|
||||
const result = getHostsQuickFiltersConfig(false);
|
||||
|
||||
expect(result[0].attributeKey.key).toBe('host_name');
|
||||
expect(result[1].attributeKey.key).toBe('os_type');
|
||||
expect(result[0].aggregateAttribute).toBe('system_cpu_load_average_15m');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,160 +1,22 @@
|
||||
import React from 'react';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Progress, TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
|
||||
import { Tag, Tooltip } from 'antd';
|
||||
import { HostData } from 'api/infraMonitoring/getHostLists';
|
||||
import { K8sRenderedRowData } from 'container/InfraMonitoringK8s/Base/types';
|
||||
import { IEntityColumn } from 'container/InfraMonitoringK8s/Base/useInfraMonitoringTableColumnsStore';
|
||||
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
|
||||
import { getGroupByEl } from 'container/InfraMonitoringK8s/Base/utils';
|
||||
import {
|
||||
getGroupByEl,
|
||||
getGroupedByMeta,
|
||||
getRowKey,
|
||||
} from 'container/InfraMonitoringK8s/Base/utils';
|
||||
import { ValidateColumnValueWrapper } from 'container/InfraMonitoringK8s/commonUtils';
|
||||
EntityProgressBar,
|
||||
ExpandButtonWrapper,
|
||||
ValidateColumnValueWrapper,
|
||||
} from 'container/InfraMonitoringK8s/components';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { useInfraMonitoringGroupBy } from 'container/InfraMonitoringK8s/hooks';
|
||||
import EntityGroupHeader from 'container/InfraMonitoringK8s/Base/EntityGroupHeader';
|
||||
|
||||
import { getMemoryProgressColor, getProgressColor } from './constants';
|
||||
import { HostnameCell } from './utils';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export const hostColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Host group',
|
||||
value: 'hostGroup',
|
||||
id: 'hostGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'Hostname',
|
||||
value: 'hostName',
|
||||
id: 'hostName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
value: 'active',
|
||||
id: 'active',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Memory Usage',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'IOWait',
|
||||
value: 'wait',
|
||||
id: 'wait',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Load Avg',
|
||||
value: 'load15',
|
||||
id: 'load15',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
|
||||
export const hostColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> HOST GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'hostGroup',
|
||||
key: 'hostGroup',
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
sorter: false,
|
||||
},
|
||||
{
|
||||
title: <div className={styles.hostnameColumnHeader}>Hostname</div>,
|
||||
dataIndex: 'hostName',
|
||||
key: 'hostName',
|
||||
width: 250,
|
||||
render: (_value, record): React.ReactNode => (
|
||||
<HostnameCell
|
||||
hostName={typeof record.hostName === 'string' ? record.hostName : ''}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className={styles.statusHeader}>
|
||||
Status
|
||||
<Tooltip title="Sent system metrics in last 10 mins">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'active',
|
||||
key: 'active',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: <div className={styles.columnHeaderRight}>CPU Usage</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className={`${styles.columnHeaderRight} ${styles.memoryUsageHeader}`}>
|
||||
Memory Usage
|
||||
<Tooltip title="Excluding cache memory">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: <div className={styles.columnHeaderRight}>IOWait</div>,
|
||||
dataIndex: 'wait',
|
||||
key: 'wait',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: <div className={styles.columnHeaderRight}>Load Avg</div>,
|
||||
dataIndex: 'load15',
|
||||
key: 'load15',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
align: 'right',
|
||||
},
|
||||
];
|
||||
import { Container } from 'lucide-react';
|
||||
|
||||
function hostRowSource(host: HostData): { meta: Record<string, string> } {
|
||||
return {
|
||||
@@ -168,67 +30,154 @@ function hostRowSource(host: HostData): { meta: Record<string, string> } {
|
||||
};
|
||||
}
|
||||
|
||||
export const hostRenderRowData = (
|
||||
host: HostData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => {
|
||||
const synthetic = hostRowSource(host);
|
||||
const rowKey = getRowKey(synthetic, () => host.hostName || 'unknown', groupBy);
|
||||
const groupedByMeta = getGroupedByMeta(synthetic, groupBy);
|
||||
const cpuPercent = Number((host.cpu * 100).toFixed(1));
|
||||
const memoryPercent = Number((host.memory * 100).toFixed(1));
|
||||
export function getHostRowKey(host: HostData): string {
|
||||
return host.hostName || 'unknown';
|
||||
}
|
||||
|
||||
return {
|
||||
key: rowKey,
|
||||
itemKey: host.hostName ?? '',
|
||||
groupedByMeta,
|
||||
meta: synthetic.meta,
|
||||
hostGroup: getGroupByEl(synthetic, groupBy),
|
||||
...synthetic.meta,
|
||||
hostName: host.hostName ?? '',
|
||||
active: (
|
||||
<Tag
|
||||
bordered
|
||||
className={`${styles.statusTag} ${
|
||||
host.active ? styles.statusTagActive : styles.statusTagInactive
|
||||
}`}
|
||||
>
|
||||
{host.active ? 'ACTIVE' : 'INACTIVE'}
|
||||
</Tag>
|
||||
export function getHostItemKey(host: HostData): string {
|
||||
return host.hostName ?? '';
|
||||
}
|
||||
|
||||
function HostGroupCell({ row }: { row: HostData }): JSX.Element {
|
||||
const [groupBy] = useInfraMonitoringGroupBy();
|
||||
const synthetic = hostRowSource(row);
|
||||
return getGroupByEl(synthetic, groupBy) as JSX.Element;
|
||||
}
|
||||
|
||||
export const hostColumnsConfig: TableColumnDef<HostData>[] = [
|
||||
{
|
||||
id: 'hostGroup',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="HOST GROUP" />,
|
||||
accessorFn: (row): string => row.hostName ?? '',
|
||||
width: { min: 300 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-collapse',
|
||||
cell: ({ row, isExpanded, toggleExpanded }): React.ReactNode => (
|
||||
<ExpandButtonWrapper isExpanded={isExpanded} toggleExpanded={toggleExpanded}>
|
||||
<HostGroupCell row={row} />
|
||||
</ExpandButtonWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<div className={styles.progressContainer}>
|
||||
<ValidateColumnValueWrapper
|
||||
value={host.cpu}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
>
|
||||
<Progress
|
||||
percent={cpuPercent}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
strokeColor={getProgressColor(cpuPercent)}
|
||||
className={styles.progressBar}
|
||||
/>
|
||||
</ValidateColumnValueWrapper>
|
||||
},
|
||||
{
|
||||
id: 'hostName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader title="Hostname" icon={<Container size={14} />} />
|
||||
),
|
||||
accessorFn: (row): string => row.hostName ?? '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
<HostnameCell hostName={value as string} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'active',
|
||||
header: (): React.ReactNode => (
|
||||
<div className={styles.statusHeader}>
|
||||
Status
|
||||
<Tooltip title="Sent system metrics in last 10 mins">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
memory: (
|
||||
<div className={styles.progressContainer}>
|
||||
<ValidateColumnValueWrapper
|
||||
value={host.memory}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
accessorFn: (row): boolean => row.active,
|
||||
width: { min: 150, default: 150 },
|
||||
enableSort: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const active = value as boolean;
|
||||
return (
|
||||
<Tag
|
||||
bordered
|
||||
className={`${styles.statusTag} ${
|
||||
active ? styles.statusTagActive : styles.statusTagInactive
|
||||
}`}
|
||||
>
|
||||
<Progress
|
||||
percent={memoryPercent}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
strokeColor={getMemoryProgressColor(memoryPercent)}
|
||||
className={styles.progressBar}
|
||||
/>
|
||||
</ValidateColumnValueWrapper>
|
||||
{active ? 'ACTIVE' : 'INACTIVE'}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
header: (): React.ReactNode => (
|
||||
<div className={styles.columnHeaderRight}>CPU Usage</div>
|
||||
),
|
||||
accessorFn: (row): number => row.cpu,
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
return (
|
||||
<div className={styles.progressContainer}>
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpu}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
>
|
||||
<EntityProgressBar value={cpu} type="cpu" />
|
||||
</ValidateColumnValueWrapper>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
header: (): React.ReactNode => (
|
||||
<div className={`${styles.columnHeaderRight} ${styles.memoryUsageHeader}`}>
|
||||
Memory Usage
|
||||
<Tooltip title="Excluding cache memory">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
wait: `${Number((host.wait * 100).toFixed(1))}%`,
|
||||
load15: host.load15,
|
||||
};
|
||||
};
|
||||
accessorFn: (row): number => row.memory,
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<div className={styles.progressContainer}>
|
||||
<ValidateColumnValueWrapper
|
||||
value={memory}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
>
|
||||
<EntityProgressBar value={memory} type="memory" />
|
||||
</ValidateColumnValueWrapper>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'wait',
|
||||
header: (): React.ReactNode => (
|
||||
<div className={styles.columnHeaderRight}>IOWait</div>
|
||||
),
|
||||
accessorFn: (row): number => row.wait,
|
||||
width: { min: 100, default: 100 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const wait = value as number;
|
||||
return (
|
||||
<TanStackTable.Text>{`${Number((wait * 100).toFixed(1))}%`}</TanStackTable.Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'load15',
|
||||
header: (): React.ReactNode => (
|
||||
<div className={styles.columnHeaderRight}>Load Avg</div>
|
||||
),
|
||||
accessorFn: (row): number => row.load15,
|
||||
width: { min: 100, default: 100 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
<TanStackTable.Text>{value as number}</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.entityGroupHeader {
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-5);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Group } from 'lucide-react';
|
||||
|
||||
import styles from './EntityGroupHeader.module.scss';
|
||||
|
||||
interface EntityGroupHeaderProps {
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
function EntityGroupHeader({
|
||||
title,
|
||||
icon,
|
||||
}: EntityGroupHeaderProps): JSX.Element {
|
||||
return (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
{icon || <Group size={14} data-hide-expanded="true" />} {title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EntityGroupHeader;
|
||||
@@ -37,10 +37,7 @@ import {
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { isCustomTimeRange, useGlobalTimeStore } from 'store/globalTime';
|
||||
import {
|
||||
getAutoRefreshQueryKey,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime/utils';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
@@ -190,16 +187,19 @@ function K8sBaseDetails<T>({
|
||||
);
|
||||
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const lastComputedMinMax = useGlobalTimeStore((s) => s.lastComputedMinMax);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
const getAutoRefreshQueryKey = useGlobalTimeStore(
|
||||
(s) => s.getAutoRefreshQueryKey,
|
||||
);
|
||||
|
||||
const { startMs, endMs } = useMemo(() => {
|
||||
const { minTime: startNs, maxTime: endNs } = getMinMaxTime(selectedTime);
|
||||
|
||||
return {
|
||||
startMs: Math.floor(startNs / NANO_SECOND_MULTIPLIER),
|
||||
endMs: Math.floor(endNs / NANO_SECOND_MULTIPLIER),
|
||||
};
|
||||
}, [getMinMaxTime, selectedTime]);
|
||||
const { startMs, endMs } = useMemo(
|
||||
() => ({
|
||||
startMs: Math.floor(lastComputedMinMax.minTime / NANO_SECOND_MULTIPLIER),
|
||||
endMs: Math.floor(lastComputedMinMax.maxTime / NANO_SECOND_MULTIPLIER),
|
||||
}),
|
||||
[lastComputedMinMax],
|
||||
);
|
||||
|
||||
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
||||
startTime: startMs,
|
||||
@@ -246,7 +246,7 @@ function K8sBaseDetails<T>({
|
||||
`${queryKeyPrefix}EntityDetails`,
|
||||
selectedItem,
|
||||
),
|
||||
[queryKeyPrefix, selectedItem, selectedTime],
|
||||
[getAutoRefreshQueryKey, queryKeyPrefix, selectedItem, selectedTime],
|
||||
);
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,175 +1,40 @@
|
||||
.clickableRow {
|
||||
cursor: pointer;
|
||||
.emptyStateContainer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.k8SListTable {
|
||||
--k8s-base-list-pagination-offset: 64px;
|
||||
padding-left: var(--spacing-2);
|
||||
--tanstack-table-header-cell-bg: var(--l2-background);
|
||||
--tanstack-table-header-cell-color: var(--l2-foreground);
|
||||
--tanstack-table-cell-bg: var(--l2-background);
|
||||
--tanstack-table-cell-color: var(--l2-foreground);
|
||||
--tanstack-table-row-hover-bg: var(--l2-background-hover);
|
||||
--tanstack-table-row-active-bg: var(--l2-background-active);
|
||||
--tanstack-table-resize-handle-bg: var(--l2-background);
|
||||
--tanstack-table-resize-handle-hover-bg: var(--l2-border);
|
||||
--tanstack-table-row-height: 42px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--tanstack-cell-padding-top-override: 5px;
|
||||
--tanstack-cell-padding-bottom-override: 5px;
|
||||
--tanstack-cell-padding-left-override: 5px;
|
||||
--tanstack-cell-padding-right-override: 5px;
|
||||
|
||||
:global(.ant-spin-nested-loading) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
--tanstack-expansion-first-col-padding-left: 30px;
|
||||
|
||||
:global(.ant-spin-container) {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: var(--k8s-base-list-pagination-offset);
|
||||
}
|
||||
|
||||
:global(.ant-table-container) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:global(.ant-table-header) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:global(.ant-table-body) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
:global(.ant-table) {
|
||||
flex: 1;
|
||||
background: var(--l1-background) !important;
|
||||
|
||||
:global(.ant-table-thead > tr > th) {
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
border-bottom: none;
|
||||
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
background: var(--l1-background) !important;
|
||||
|
||||
&::before {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-table-cell) {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l1-background);
|
||||
border-bottom: none;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.progress-container) {
|
||||
:global(.ant-progress-bg) {
|
||||
height: 8px !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-table-tbody > tr:hover > td) {
|
||||
background: var(--l1-background-hover);
|
||||
}
|
||||
|
||||
:global(.ant-table-tbody > tr:not(.ant-table-expanded-row):hover > td) {
|
||||
background: var(--l1-background-hover) !important;
|
||||
}
|
||||
|
||||
:global(.ant-table-tbody > tr.ant-table-expanded-row:hover > td) {
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
:global(.ant-table-tbody > tr > td) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:global(.ant-empty-normal) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
:global(.ant-table-cell) {
|
||||
min-width: 180px !important;
|
||||
max-width: 180px !important;
|
||||
}
|
||||
|
||||
:global(.ant-table-cell:first-of-type):not(
|
||||
:global(.ant-table-row-expand-icon-cell)
|
||||
) {
|
||||
min-width: 250px !important;
|
||||
max-width: 250px !important;
|
||||
}
|
||||
|
||||
:global(.ant-table-row-expand-icon-cell) {
|
||||
min-width: 40px !important;
|
||||
max-width: 40px !important;
|
||||
}
|
||||
|
||||
:global(.ant-table-content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: color-mix(var(--accent-primary), transparent 40%);
|
||||
}
|
||||
}
|
||||
&[data-has-group-by='false'] {
|
||||
--tanstack-cell-padding-left-first-column: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.paginationDock {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: calc(100% - 340px) !important;
|
||||
margin: 0 !important;
|
||||
padding: 16px;
|
||||
background-color: var(--l1-background);
|
||||
z-index: 1;
|
||||
.paginationContainer {
|
||||
padding-bottom: var(--spacing-8);
|
||||
padding-right: var(--spacing-8);
|
||||
|
||||
:global(.ant-pagination-item) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item-active) {
|
||||
background: var(--l2-background);
|
||||
border-color: var(--l2-border);
|
||||
|
||||
a {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,36 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Spin,
|
||||
Table,
|
||||
TableColumnType as ColumnType,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
} from 'antd';
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
import { Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import TanStackTable, {
|
||||
TableColumnDef,
|
||||
useHiddenColumnIds,
|
||||
} from 'components/TanStackTableView';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import {
|
||||
getAutoRefreshQueryKey,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime/utils';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringFilters,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
import {
|
||||
useInfraMonitoringFiltersK8s,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringPageListing,
|
||||
useInfraMonitoringPageSizeListing,
|
||||
} from '../hooks';
|
||||
import { usePageSize } from '../utils';
|
||||
import { K8sEmptyState } from './K8sEmptyState';
|
||||
import { K8sExpandedRow } from './K8sExpandedRow';
|
||||
import K8sHeader from './K8sHeader';
|
||||
import { K8sBaseFilters, K8sRenderedRowData } from './types';
|
||||
import {
|
||||
IEntityColumn,
|
||||
useInfraMonitoringTableColumnsForPage,
|
||||
useInfraMonitoringTableColumnsStore,
|
||||
} from './useInfraMonitoringTableColumnsStore';
|
||||
import { K8sBaseFilters } from './types';
|
||||
import { getGroupedByMeta } from './utils';
|
||||
|
||||
import styles from './K8sBaseList.module.scss';
|
||||
import cx from 'classnames';
|
||||
|
||||
export type K8sBaseListEmptyStateContext = {
|
||||
isError: boolean;
|
||||
@@ -53,11 +41,13 @@ export type K8sBaseListEmptyStateContext = {
|
||||
rawData?: unknown;
|
||||
};
|
||||
|
||||
export type K8sBaseListProps<T = unknown> = {
|
||||
/** Base type constraint for K8s entity data */
|
||||
export type K8sEntityData = { meta?: Record<string, string> };
|
||||
|
||||
export type K8sBaseListProps<T extends K8sEntityData> = {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
entity: InfraMonitoringEntity;
|
||||
tableColumnsDefinitions: IEntityColumn[];
|
||||
tableColumns: ColumnType<K8sRenderedRowData>[];
|
||||
tableColumns: TableColumnDef<T>[];
|
||||
fetchListData: (
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
@@ -67,69 +57,63 @@ export type K8sBaseListProps<T = unknown> = {
|
||||
error?: string | null;
|
||||
rawData?: unknown;
|
||||
}>;
|
||||
renderRowData: (
|
||||
record: T,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
) => K8sRenderedRowData;
|
||||
/** Function to get the unique key for a row. */
|
||||
getRowKey?: (record: T) => string;
|
||||
/** Function to get the item key used for selection. Defaults to getRowKey if not provided. */
|
||||
getItemKey?: (record: T) => string;
|
||||
eventCategory: InfraMonitoringEvents;
|
||||
renderEmptyState?: (
|
||||
context: K8sBaseListEmptyStateContext,
|
||||
) => React.ReactNode | null;
|
||||
};
|
||||
|
||||
export function K8sBaseList<T>({
|
||||
export function K8sBaseList<T extends K8sEntityData>({
|
||||
controlListPrefix,
|
||||
entity,
|
||||
tableColumnsDefinitions,
|
||||
tableColumns,
|
||||
fetchListData,
|
||||
renderRowData,
|
||||
getRowKey,
|
||||
getItemKey,
|
||||
eventCategory,
|
||||
renderEmptyState,
|
||||
}: K8sBaseListProps<T>): JSX.Element {
|
||||
const [queryFilters] = useInfraMonitoringFilters();
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [queryFilters] = useInfraMonitoringFiltersK8s();
|
||||
const [currentPage] = useInfraMonitoringPageListing();
|
||||
const [currentPageSize] = useInfraMonitoringPageSizeListing();
|
||||
const [groupBy] = useInfraMonitoringGroupBy();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
const [initialOrderBy] = useState(orderBy);
|
||||
const [orderBy] = useInfraMonitoringOrderBy();
|
||||
const [selectedItem, setSelectedItem] = useQueryState(
|
||||
'selectedItem',
|
||||
parseAsString,
|
||||
);
|
||||
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
setExpandedRowKeys([]);
|
||||
}, [groupBy, currentPage]);
|
||||
const { pageSize, setPageSize } = usePageSize(entity);
|
||||
|
||||
const initializeTableColumns = useInfraMonitoringTableColumnsStore(
|
||||
(state) => state.initializePageColumns,
|
||||
);
|
||||
useEffect(() => {
|
||||
initializeTableColumns(entity, tableColumnsDefinitions);
|
||||
}, [initializeTableColumns, entity, tableColumnsDefinitions]);
|
||||
const columnStorageKey = `k8s-${entity}-columns`;
|
||||
const hiddenColumnIds = useHiddenColumnIds(columnStorageKey);
|
||||
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
|
||||
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
const getAutoRefreshQueryKey = useGlobalTimeStore(
|
||||
(s) => s.getAutoRefreshQueryKey,
|
||||
);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
return getAutoRefreshQueryKey(
|
||||
selectedTime,
|
||||
'k8sBaseList',
|
||||
entity,
|
||||
String(pageSize),
|
||||
String(currentPageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
);
|
||||
}, [
|
||||
getAutoRefreshQueryKey,
|
||||
selectedTime,
|
||||
entity,
|
||||
pageSize,
|
||||
currentPageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
@@ -143,8 +127,8 @@ export function K8sBaseList<T>({
|
||||
|
||||
return fetchListData(
|
||||
{
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
limit: currentPageSize,
|
||||
offset: (currentPage - 1) * currentPageSize,
|
||||
filters: queryFilters || { items: [], op: 'AND' },
|
||||
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
|
||||
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
|
||||
@@ -157,62 +141,14 @@ export function K8sBaseList<T>({
|
||||
refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
});
|
||||
|
||||
const pageData = data?.data;
|
||||
const pageData = data?.data ?? [];
|
||||
const totalCount = data?.total || 0;
|
||||
const hasFilters = (queryFilters?.items?.length ?? 0) > 0;
|
||||
|
||||
const formattedItemsData = useMemo(() => {
|
||||
if (!pageData) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rows = pageData.map((item) => renderRowData(item, groupBy));
|
||||
|
||||
// Without handling duplicated keys, the table became unpredictable/unstable
|
||||
const keyCount = new Map<string, number>();
|
||||
return rows.map(
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
(row): K8sRenderedRowData => {
|
||||
const count = keyCount.get(row.key) || 0;
|
||||
keyCount.set(row.key, count + 1);
|
||||
|
||||
if (count > 0) {
|
||||
return { ...row, key: `${row.key}-${count}` };
|
||||
}
|
||||
return row;
|
||||
},
|
||||
);
|
||||
}, [pageData, renderRowData, groupBy]);
|
||||
|
||||
const handleTableChange: TableProps<K8sRenderedRowData>['onChange'] =
|
||||
useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter:
|
||||
| SorterResult<K8sRenderedRowData>
|
||||
| SorterResult<K8sRenderedRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
[eventCategory, setCurrentPage, setOrderBy],
|
||||
);
|
||||
const getGroupKeyFn = useCallback(
|
||||
(item: T) => getGroupedByMeta(item, groupBy),
|
||||
[groupBy],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
@@ -223,214 +159,137 @@ export function K8sBaseList<T>({
|
||||
});
|
||||
}, [eventCategory, totalCount]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sRenderedRowData): void => {
|
||||
if (expandedRowKeys.includes(record.key)) {
|
||||
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
|
||||
} else {
|
||||
setExpandedRowKeys([record.key]);
|
||||
}
|
||||
};
|
||||
|
||||
const openItemInNewTab = (record: K8sRenderedRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set('selectedItem', record.itemKey);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sRenderedRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openItemInNewTab(record);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedItem(record.itemKey);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
};
|
||||
|
||||
const [columnsDefinitions, columnsHidden] =
|
||||
useInfraMonitoringTableColumnsForPage(entity);
|
||||
|
||||
const hiddenColumnIdsOnList = useMemo(
|
||||
() =>
|
||||
columnsDefinitions
|
||||
.filter(
|
||||
(col) =>
|
||||
(groupBy?.length > 0 && col.behavior === 'hidden-on-expand') ||
|
||||
(!groupBy?.length && col.behavior === 'hidden-on-collapse'),
|
||||
)
|
||||
.map((col) => col.id),
|
||||
[columnsDefinitions, groupBy?.length],
|
||||
);
|
||||
|
||||
const mapDefaultSort = useCallback(
|
||||
(
|
||||
tableColumn: ColumnType<K8sRenderedRowData>,
|
||||
): ColumnType<K8sRenderedRowData> => {
|
||||
if (tableColumn.key === initialOrderBy?.columnName) {
|
||||
return {
|
||||
...tableColumn,
|
||||
defaultSortOrder: initialOrderBy?.order === 'asc' ? 'ascend' : 'descend',
|
||||
};
|
||||
const handleRowClick = useCallback(
|
||||
(_record: T, itemKey: string): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedItem(itemKey);
|
||||
}
|
||||
|
||||
return tableColumn;
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
},
|
||||
[initialOrderBy?.columnName, initialOrderBy?.order],
|
||||
[eventCategory, groupBy.length, setSelectedItem],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
tableColumns
|
||||
.filter(
|
||||
(c) =>
|
||||
!hiddenColumnIdsOnList.includes(c.key?.toString() || '') &&
|
||||
!columnsHidden.includes(c.key?.toString() || ''),
|
||||
)
|
||||
.map(mapDefaultSort),
|
||||
[columnsHidden, hiddenColumnIdsOnList, mapDefaultSort, tableColumns],
|
||||
const handleRowClickNewTab = useCallback(
|
||||
(_record: T, itemKey: string): void => {
|
||||
if (groupBy.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build URL with selectedItem param
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('selectedItem', itemKey);
|
||||
openInNewTab(url.pathname + url.search);
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
},
|
||||
[eventCategory, groupBy.length],
|
||||
);
|
||||
|
||||
const isGroupedByAttribute = groupBy.length > 0;
|
||||
|
||||
const expandedRowRender = (record: K8sRenderedRowData): JSX.Element => (
|
||||
<K8sExpandedRow<T>
|
||||
record={record}
|
||||
entity={entity}
|
||||
tableColumns={tableColumns}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={renderRowData}
|
||||
/>
|
||||
// Filter columns for expanded row based on parent's hidden columns
|
||||
const expandedRowColumns = useMemo(
|
||||
() => tableColumns.filter((col) => !hiddenColumnIds.includes(col.id)),
|
||||
[tableColumns, hiddenColumnIds],
|
||||
);
|
||||
|
||||
const expandRowIconRenderer = ({
|
||||
expanded,
|
||||
onExpand,
|
||||
record,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
onExpand: (
|
||||
record: K8sRenderedRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sRenderedRowData;
|
||||
}): JSX.Element | null => {
|
||||
if (!isGroupedByAttribute) {
|
||||
return null;
|
||||
}
|
||||
const renderExpandedRow = useCallback(
|
||||
(
|
||||
_record: T,
|
||||
rowKey: string,
|
||||
groupMeta?: Record<string, string>,
|
||||
): JSX.Element => (
|
||||
<K8sExpandedRow<T>
|
||||
rowKey={rowKey}
|
||||
groupMeta={groupMeta}
|
||||
entity={entity}
|
||||
tableColumns={expandedRowColumns}
|
||||
fetchListData={fetchListData}
|
||||
getRowKey={getRowKey}
|
||||
getItemKey={getItemKey}
|
||||
/>
|
||||
),
|
||||
[entity, fetchListData, getRowKey, getItemKey, expandedRowColumns],
|
||||
);
|
||||
|
||||
return expanded ? (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronDown size={14} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const onPaginationChange = (page: number, pageSize: number): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
};
|
||||
const getRowCanExpand = useCallback(
|
||||
(): boolean => isGroupedByAttribute,
|
||||
[isGroupedByAttribute],
|
||||
);
|
||||
|
||||
const showTableLoadingState = isLoading;
|
||||
|
||||
const emptyStateContext: K8sBaseListEmptyStateContext = {
|
||||
isError: isError || !!data?.error,
|
||||
const emptyTableMessage: React.ReactNode = renderEmptyState?.({
|
||||
isError,
|
||||
error: data?.error,
|
||||
totalCount,
|
||||
hasFilters,
|
||||
isLoading: showTableLoadingState,
|
||||
rawData: data?.rawData,
|
||||
};
|
||||
|
||||
const emptyTableMessage: React.ReactNode = renderEmptyState?.(
|
||||
emptyStateContext,
|
||||
) || (
|
||||
}) || (
|
||||
<K8sEmptyState
|
||||
isError={emptyStateContext.isError}
|
||||
error={emptyStateContext.error}
|
||||
isLoading={emptyStateContext.isLoading}
|
||||
rawData={emptyStateContext.rawData}
|
||||
isError={isError}
|
||||
error={data?.error}
|
||||
isLoading={showTableLoadingState}
|
||||
rawData={data?.rawData}
|
||||
/>
|
||||
);
|
||||
|
||||
const showEmptyState = !showTableLoadingState && pageData.length === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sHeader
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={entity}
|
||||
showAutoRefresh={!selectedItem}
|
||||
columns={tableColumns}
|
||||
columnStorageKey={columnStorageKey}
|
||||
/>
|
||||
<Table
|
||||
className={styles.k8SListTable}
|
||||
dataSource={showTableLoadingState ? [] : formattedItemsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: onPaginationChange,
|
||||
className: styles.paginationDock,
|
||||
}}
|
||||
loading={{
|
||||
spinning: showTableLoadingState,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
locale={{
|
||||
emptyText: showTableLoadingState ? null : emptyTableMessage,
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: styles.clickableRow,
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
expandIcon: expandRowIconRenderer,
|
||||
expandedRowKeys,
|
||||
}}
|
||||
/>
|
||||
{isError && (
|
||||
<Typography>{data?.error?.toString() || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
|
||||
{showEmptyState ? (
|
||||
<div className={styles.emptyStateContainer}>{emptyTableMessage}</div>
|
||||
) : (
|
||||
<TanStackTable<T>
|
||||
data={pageData}
|
||||
columns={tableColumns}
|
||||
columnStorageKey={columnStorageKey}
|
||||
isLoading={showTableLoadingState}
|
||||
getRowKey={getRowKey}
|
||||
getItemKey={getItemKey}
|
||||
groupBy={groupBy}
|
||||
getGroupKey={getGroupKeyFn}
|
||||
onRowClick={handleRowClick}
|
||||
onRowClickNewTab={handleRowClickNewTab}
|
||||
renderExpandedRow={isGroupedByAttribute ? renderExpandedRow : undefined}
|
||||
getRowCanExpand={isGroupedByAttribute ? getRowCanExpand : undefined}
|
||||
className={cx(styles.k8SListTable, expandedRowColumns)}
|
||||
enableQueryParams={{
|
||||
page: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
|
||||
limit: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,
|
||||
orderBy: INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
|
||||
expanded: INFRA_MONITORING_K8S_PARAMS_KEYS.EXPANDED,
|
||||
}}
|
||||
pagination={{
|
||||
total: totalCount,
|
||||
defaultLimit: 10,
|
||||
defaultPage: 1,
|
||||
}}
|
||||
paginationClassname={styles.paginationContainer}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
.title {
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
font-size: var(--periscope-font-size-medium);
|
||||
}
|
||||
|
||||
.message {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AlertTriangle, LifeBuoy } from 'lucide-react';
|
||||
import emptyStateUrl from '@/assets/Icons/emptyState.svg';
|
||||
import eyesEmojiUrl from '@/assets/Images/eyesEmoji.svg';
|
||||
|
||||
import { K8sBaseListEmptyStateContext } from './K8sBaseList';
|
||||
import type { K8sBaseListEmptyStateContext } from './K8sBaseList';
|
||||
|
||||
import styles from './K8sEmptyState.module.scss';
|
||||
|
||||
|
||||
@@ -1,28 +1,35 @@
|
||||
.expandedClickableRow {
|
||||
cursor: pointer;
|
||||
.expandedTableContainer {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.expandedTableContainer {
|
||||
border: 1px solid var(--l1-border);
|
||||
overflow-x: auto;
|
||||
padding-left: 48px;
|
||||
.expandedTable {
|
||||
--tanstack-table-header-cell-bg: var(--l1-background);
|
||||
--tanstack-table-header-cell-color: var(--l1-foreground);
|
||||
--tanstack-table-cell-bg: var(--l1-background);
|
||||
--tanstack-table-cell-color: var(--l1-foreground);
|
||||
--tanstack-table-row-hover-bg: var(--l1-background-hover);
|
||||
--tanstack-table-row-active-bg: var(--l1-background-active);
|
||||
--tanstack-table-resize-handle-bg: var(--l1-background);
|
||||
--tanstack-table-resize-handle-hover-bg: var(--l1-border);
|
||||
--tanstack-table-row-height: 36px;
|
||||
|
||||
:global(.ant-table-tbody > tr:hover > td) {
|
||||
background: none !important;
|
||||
--tanstack-cell-padding-left-override: 15px;
|
||||
--tanstack-cell-padding-right-override: 15px;
|
||||
|
||||
& [data-hide-expanded='true'] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.expandedTableFooter {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
padding-left: 42px;
|
||||
margin-top: 8px;
|
||||
gap: var(--spacing-4);
|
||||
background-color: var(--l1-background);
|
||||
}
|
||||
|
||||
.viewAllButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
margin: var(--spacing-4);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,40 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Spin,
|
||||
Table,
|
||||
TableColumnType as ColumnType,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { Button } from '@signozhq/ui';
|
||||
import { Typography } from 'antd';
|
||||
import TanStackTable, {
|
||||
SortState,
|
||||
TableColumnDef,
|
||||
TanStackTableStateProvider,
|
||||
} from 'components/TanStackTableView';
|
||||
import { CornerDownRight } from 'lucide-react';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import {
|
||||
getAutoRefreshQueryKey,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime/utils';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringFilters,
|
||||
useInfraMonitoringFiltersK8s,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringPageListing,
|
||||
useInfraMonitoringSelectedItem,
|
||||
} from '../hooks';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
import { K8sBaseFilters, K8sRenderedRowData } from './types';
|
||||
import { useInfraMonitoringTableColumnsForPage } from './useInfraMonitoringTableColumnsStore';
|
||||
import { K8sBaseFilters } from './types';
|
||||
|
||||
import styles from './K8sExpandedRow.module.scss';
|
||||
|
||||
const EXPANDED_ROW_LIMIT = 10;
|
||||
|
||||
export type K8sExpandedRowProps<T> = {
|
||||
record: K8sRenderedRowData;
|
||||
/** Pre-computed row key from parent table (includes group prefix + duplicate handling) */
|
||||
rowKey: string;
|
||||
/** Group metadata for building filters */
|
||||
groupMeta?: Record<string, string>;
|
||||
entity: InfraMonitoringEntity;
|
||||
tableColumns: ColumnType<K8sRenderedRowData>[];
|
||||
tableColumns: TableColumnDef<T>[];
|
||||
fetchListData: (
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
@@ -47,47 +44,46 @@ export type K8sExpandedRowProps<T> = {
|
||||
error?: string | null;
|
||||
rawData?: unknown;
|
||||
}>;
|
||||
renderRowData: (
|
||||
record: T,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
) => K8sRenderedRowData;
|
||||
/** Function to get the unique key for a row. */
|
||||
getRowKey?: (record: T) => string;
|
||||
/** Function to get the item key used for selection. Defaults to getRowKey if not provided. */
|
||||
getItemKey?: (record: T) => string;
|
||||
};
|
||||
|
||||
export const MAX_ITEMS_TO_FETCH_WHEN_GROUP_BY = 10;
|
||||
|
||||
export function K8sExpandedRow<T>({
|
||||
record,
|
||||
rowKey,
|
||||
groupMeta,
|
||||
entity,
|
||||
tableColumns,
|
||||
fetchListData,
|
||||
renderRowData,
|
||||
getRowKey,
|
||||
getItemKey,
|
||||
}: K8sExpandedRowProps<T>): JSX.Element {
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
const [, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [queryFilters, setFilters] = useInfraMonitoringFilters();
|
||||
const [, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [, setCurrentPage] = useInfraMonitoringPageListing();
|
||||
const [queryFilters, setFilters] = useInfraMonitoringFiltersK8s();
|
||||
const [, setSelectedItem] = useInfraMonitoringSelectedItem();
|
||||
const [, setMainOrderBy] = useInfraMonitoringOrderBy();
|
||||
|
||||
const [columnsDefinitions, columnsHidden] =
|
||||
useInfraMonitoringTableColumnsForPage(entity);
|
||||
|
||||
const hiddenColumnIdsForNested = useMemo(
|
||||
() =>
|
||||
columnsDefinitions
|
||||
.filter((col) => col.behavior === 'hidden-on-collapse')
|
||||
.map((col) => col.id),
|
||||
[columnsDefinitions],
|
||||
const orderByParamKey = useMemo(
|
||||
() => `orderBy_${rowKey.replace(/[^a-zA-Z0-9]/g, '_')}`,
|
||||
[rowKey],
|
||||
);
|
||||
|
||||
const nestedColumns = useMemo(
|
||||
() =>
|
||||
tableColumns.filter(
|
||||
(c) =>
|
||||
!columnsHidden.includes(c.key?.toString() || '') &&
|
||||
!hiddenColumnIdsForNested.includes(c.key?.toString() || ''),
|
||||
),
|
||||
[tableColumns, columnsHidden, hiddenColumnIdsForNested],
|
||||
const [orderBy, setOrderBy] = useQueryState(
|
||||
orderByParamKey,
|
||||
parseAsJsonNoValidate<SortState | null>()
|
||||
.withDefault(null as never)
|
||||
.withOptions({
|
||||
history: 'push',
|
||||
}),
|
||||
);
|
||||
useEffect(() => {
|
||||
return (): void => {
|
||||
void setOrderBy(null);
|
||||
};
|
||||
}, [setOrderBy]);
|
||||
|
||||
const storageKey = `k8s-${entity}-columns-expanded`;
|
||||
|
||||
const createFiltersForRecord = useCallback((): NonNullable<
|
||||
IBuilderQuery['filters']
|
||||
@@ -97,45 +93,64 @@ export function K8sExpandedRow<T>({
|
||||
op: 'and',
|
||||
};
|
||||
|
||||
const { groupedByMeta } = record;
|
||||
const metaKeys = groupMeta ?? {};
|
||||
|
||||
for (const key of Object.keys(groupedByMeta)) {
|
||||
for (const key of Object.keys(metaKeys)) {
|
||||
const value = metaKeys[key];
|
||||
// Skip empty values to avoid creating invalid filters
|
||||
if (value === '' || value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
baseFilters.items.push({
|
||||
key: {
|
||||
key,
|
||||
type: null,
|
||||
type: 'resource',
|
||||
},
|
||||
op: '=',
|
||||
value: groupedByMeta[key],
|
||||
value,
|
||||
id: key,
|
||||
});
|
||||
}
|
||||
|
||||
return baseFilters;
|
||||
}, [queryFilters?.items, record]);
|
||||
}, [queryFilters?.items, groupMeta]);
|
||||
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
|
||||
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
const getAutoRefreshQueryKey = useGlobalTimeStore(
|
||||
(s) => s.getAutoRefreshQueryKey,
|
||||
);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
return getAutoRefreshQueryKey(selectedTime, [
|
||||
return getAutoRefreshQueryKey(
|
||||
selectedTime,
|
||||
entity,
|
||||
'k8sExpandedRow',
|
||||
record.key,
|
||||
JSON.stringify(groupMeta),
|
||||
rowKey,
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
]);
|
||||
}, [selectedTime, record.key, queryFilters, orderBy]);
|
||||
);
|
||||
}, [
|
||||
getAutoRefreshQueryKey,
|
||||
selectedTime,
|
||||
entity,
|
||||
groupMeta,
|
||||
rowKey,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useQuery({
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey,
|
||||
queryFn: ({ signal }) => {
|
||||
queryFn: async ({ signal }) => {
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
return fetchListData(
|
||||
return await fetchListData(
|
||||
{
|
||||
limit: MAX_ITEMS_TO_FETCH_WHEN_GROUP_BY,
|
||||
limit: EXPANDED_ROW_LIMIT,
|
||||
offset: 0,
|
||||
filters: createFiltersForRecord(),
|
||||
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
|
||||
@@ -146,48 +161,45 @@ export function K8sExpandedRow<T>({
|
||||
signal,
|
||||
);
|
||||
},
|
||||
staleTime: 1000 * 60 * 30,
|
||||
refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
});
|
||||
|
||||
const formattedData = useMemo(() => {
|
||||
if (!data?.data) {
|
||||
return undefined;
|
||||
}
|
||||
const expandedData = data?.data ?? [];
|
||||
|
||||
const rows = data.data.map((item) => renderRowData(item, groupBy));
|
||||
|
||||
// Without handling duplicated keys, the table became unpredictable/unstable
|
||||
const keyCount = new Map<string, number>();
|
||||
return rows.map((row): K8sRenderedRowData => {
|
||||
const count = keyCount.get(row.key) || 0;
|
||||
keyCount.set(row.key, count + 1);
|
||||
|
||||
if (count > 0) {
|
||||
return { ...row, key: `${row.key}-${count}` };
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}, [data?.data, renderRowData, groupBy]);
|
||||
|
||||
const openRecordInNewTab = (rowRecord: K8sRenderedRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set('selectedItem', rowRecord.itemKey);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
const handleRowClick = useCallback(
|
||||
(_row: T, itemKey: string): void => {
|
||||
setSelectedItem(itemKey);
|
||||
},
|
||||
[setSelectedItem],
|
||||
);
|
||||
|
||||
const handleViewAllClick = (): void => {
|
||||
const filters = createFiltersForRecord();
|
||||
setFilters(filters);
|
||||
setCurrentPage(1);
|
||||
setGroupBy([]);
|
||||
setOrderBy(null);
|
||||
setCurrentPage(1);
|
||||
setFilters(filters);
|
||||
if (orderBy) {
|
||||
setMainOrderBy(orderBy);
|
||||
}
|
||||
};
|
||||
|
||||
const total = data?.total ?? 0;
|
||||
const hasMoreItems = total > EXPANDED_ROW_LIMIT;
|
||||
|
||||
const footerContent = hasMoreItems ? (
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
className={styles.viewAllButton}
|
||||
onClick={handleViewAllClick}
|
||||
prefix={<CornerDownRight size={14} />}
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.expandedTableContainer}
|
||||
@@ -197,50 +209,30 @@ export function K8sExpandedRow<T>({
|
||||
<Typography>{data?.error?.toString() || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
|
||||
{isFetching || isLoading ? (
|
||||
<LoadingContainer />
|
||||
) : (
|
||||
<div data-testid="expanded-table">
|
||||
<Table
|
||||
columns={nestedColumns}
|
||||
dataSource={formattedData}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
showHeader={false}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
<div data-testid="expanded-table">
|
||||
<TanStackTableStateProvider>
|
||||
<TanStackTable<T>
|
||||
data={expandedData}
|
||||
columns={tableColumns}
|
||||
columnStorageKey={storageKey}
|
||||
isLoading={isLoading}
|
||||
getRowKey={getRowKey}
|
||||
getItemKey={getItemKey}
|
||||
onRowClick={handleRowClick}
|
||||
enableQueryParams={{
|
||||
orderBy: orderByParamKey,
|
||||
}}
|
||||
onRow={(
|
||||
rowRecord: K8sRenderedRowData,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
openRecordInNewTab(rowRecord);
|
||||
return;
|
||||
}
|
||||
setSelectedItem(rowRecord.itemKey);
|
||||
},
|
||||
className: styles.expandedClickableRow,
|
||||
})}
|
||||
tableScrollerProps={{
|
||||
className: styles.expandedTable,
|
||||
}}
|
||||
disableVirtualScroll
|
||||
cellTypographySize="medium"
|
||||
/>
|
||||
|
||||
{data?.total && data?.total > MAX_ITEMS_TO_FETCH_WHEN_GROUP_BY && (
|
||||
<div className={styles.expandedTableFooter}>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
className={styles.viewAllButton}
|
||||
onClick={handleViewAllClick}
|
||||
>
|
||||
<CornerDownRight size={14} />
|
||||
View All
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TanStackTableStateProvider>
|
||||
{!isLoading && expandedData.length > 0 && (
|
||||
<div className={styles.expandedTableFooter}>{footerContent}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,57 +1,98 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, DrawerWrapper } from '@signozhq/ui';
|
||||
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
useInfraMonitoringTableColumnsForPage,
|
||||
useInfraMonitoringTableColumnsStore,
|
||||
} from './useInfraMonitoringTableColumnsStore';
|
||||
hideColumn,
|
||||
showColumn,
|
||||
TableColumnDef,
|
||||
useHiddenColumnIds,
|
||||
} from 'components/TanStackTableView';
|
||||
|
||||
import styles from './K8sFiltersSidePanel.module.scss';
|
||||
|
||||
function K8sFiltersSidePanel({
|
||||
type ColumnPickerItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
canBeHidden: boolean;
|
||||
visibilityBehavior:
|
||||
| 'hidden-on-expand'
|
||||
| 'hidden-on-collapse'
|
||||
| 'always-visible';
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts TableColumnDef to column picker item format
|
||||
*/
|
||||
function toColumnPickerItems<T>(
|
||||
columns: TableColumnDef<T>[],
|
||||
): ColumnPickerItem[] {
|
||||
return columns.map((col) => ({
|
||||
id: col.id,
|
||||
label: typeof col.header === 'string' ? col.header : col.id,
|
||||
canBeHidden: col.canBeHidden !== false && col.enableRemove !== false,
|
||||
visibilityBehavior: col.visibilityBehavior ?? 'always-visible',
|
||||
}));
|
||||
}
|
||||
|
||||
function K8sFiltersSidePanel<TData>({
|
||||
open,
|
||||
onClose,
|
||||
entity,
|
||||
columns,
|
||||
storageKey,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
entity: InfraMonitoringEntity;
|
||||
columns: TableColumnDef<TData>[];
|
||||
storageKey: string;
|
||||
}): JSX.Element {
|
||||
const addColumn = useInfraMonitoringTableColumnsStore(
|
||||
(state) => state.addColumn,
|
||||
const columnPickerItems = useMemo(
|
||||
() => toColumnPickerItems(columns),
|
||||
[columns],
|
||||
);
|
||||
const removeColumn = useInfraMonitoringTableColumnsStore(
|
||||
(state) => state.removeColumn,
|
||||
const hiddenColumnIds = useHiddenColumnIds(storageKey);
|
||||
|
||||
const addedColumns = useMemo(
|
||||
() =>
|
||||
columnPickerItems.filter(
|
||||
(column) =>
|
||||
!hiddenColumnIds.includes(column.id) &&
|
||||
column.visibilityBehavior !== 'hidden-on-collapse',
|
||||
),
|
||||
[columnPickerItems, hiddenColumnIds],
|
||||
);
|
||||
|
||||
const [columns, columnsHidden] = useInfraMonitoringTableColumnsForPage(entity);
|
||||
const hiddenColumns = useMemo(
|
||||
() =>
|
||||
columnPickerItems.filter((column) => hiddenColumnIds.includes(column.id)),
|
||||
[columnPickerItems, hiddenColumnIds],
|
||||
);
|
||||
|
||||
const handleRemoveColumn = (columnId: string): void => {
|
||||
hideColumn(storageKey, columnId);
|
||||
};
|
||||
|
||||
const handleAddColumn = (columnId: string): void => {
|
||||
showColumn(storageKey, columnId);
|
||||
};
|
||||
|
||||
const drawerContent = (
|
||||
<>
|
||||
<div className={styles.columnsTitle}>Added Columns (Click to remove)</div>
|
||||
|
||||
<div className={styles.columnsList}>
|
||||
{columns
|
||||
.filter(
|
||||
(column) =>
|
||||
!columnsHidden.includes(column.id) &&
|
||||
column.behavior !== 'hidden-on-collapse',
|
||||
)
|
||||
.map((column) => (
|
||||
<div className={styles.columnItem} key={column.value}>
|
||||
{/*<GripVertical size={16} /> TODO: Add support back when update the table component */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="none"
|
||||
className={styles.columnItem}
|
||||
disabled={!column.canBeHidden}
|
||||
data-testid={`remove-column-${column.id}`}
|
||||
onClick={(): void => removeColumn(entity, column.id)}
|
||||
>
|
||||
{column.label}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{addedColumns.map((column) => (
|
||||
<div className={styles.columnItem} key={column.id}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="none"
|
||||
className={styles.columnItem}
|
||||
disabled={!column.canBeHidden}
|
||||
data-testid={`remove-column-${column.id}`}
|
||||
onClick={(): void => handleRemoveColumn(column.id)}
|
||||
>
|
||||
{column.label}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.horizontalDivider} />
|
||||
@@ -59,23 +100,21 @@ function K8sFiltersSidePanel({
|
||||
<div className={styles.columnsTitle}>Other Columns (Click to add)</div>
|
||||
|
||||
<div className={styles.columnsList}>
|
||||
{columns
|
||||
.filter((column) => columnsHidden.includes(column.id))
|
||||
.map((column) => (
|
||||
<div className={styles.columnItem} key={column.value}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="none"
|
||||
className={styles.columnItem}
|
||||
data-can-be-added="true"
|
||||
data-testid={`add-column-${column.id}`}
|
||||
onClick={(): void => addColumn(entity, column.id)}
|
||||
tabIndex={0}
|
||||
>
|
||||
{column.label}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{hiddenColumns.map((column) => (
|
||||
<div className={styles.columnItem} key={column.id}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="none"
|
||||
className={styles.columnItem}
|
||||
data-can-be-added="true"
|
||||
data-testid={`add-column-${column.id}`}
|
||||
onClick={(): void => handleAddColumn(column.id)}
|
||||
tabIndex={0}
|
||||
>
|
||||
{column.label}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { getGroupByEl } from './utils';
|
||||
import { useInfraMonitoringGroupBy } from '../hooks';
|
||||
|
||||
interface K8sEntityWithMeta {
|
||||
meta?: Record<string, string>;
|
||||
}
|
||||
|
||||
function K8sGroupCell<T extends K8sEntityWithMeta>({
|
||||
row,
|
||||
}: {
|
||||
row: T;
|
||||
}): JSX.Element {
|
||||
const [groupBy] = useInfraMonitoringGroupBy();
|
||||
return getGroupByEl(row, groupBy) as JSX.Element;
|
||||
}
|
||||
|
||||
export default K8sGroupCell;
|
||||
@@ -2,6 +2,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Button } from '@signozhq/ui';
|
||||
import { Select } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
@@ -19,27 +20,31 @@ import {
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringFilters,
|
||||
useInfraMonitoringFiltersK8s,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringPageListing,
|
||||
} from '../hooks';
|
||||
import K8sFiltersSidePanel from './K8sFiltersSidePanel';
|
||||
|
||||
import styles from './K8sHeader.module.scss';
|
||||
|
||||
interface K8sHeaderProps {
|
||||
interface K8sHeaderProps<TData> {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
entity: InfraMonitoringEntity;
|
||||
showAutoRefresh: boolean;
|
||||
columns: TableColumnDef<TData>[];
|
||||
columnStorageKey: string;
|
||||
}
|
||||
|
||||
function K8sHeader({
|
||||
function K8sHeader<TData>({
|
||||
controlListPrefix,
|
||||
entity,
|
||||
showAutoRefresh,
|
||||
}: K8sHeaderProps): JSX.Element {
|
||||
columns,
|
||||
columnStorageKey,
|
||||
}: K8sHeaderProps<TData>): JSX.Element {
|
||||
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFilters();
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
|
||||
|
||||
const currentQuery = initialQueriesMap[DataSource.METRICS];
|
||||
|
||||
@@ -77,7 +82,7 @@ function K8sHeader({
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const [, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [, setCurrentPage] = useInfraMonitoringPageListing();
|
||||
const handleChangeTagFilters = useCallback(
|
||||
(value: IBuilderQuery['filters']) => {
|
||||
setUrlFilters(value || null);
|
||||
@@ -207,7 +212,6 @@ function K8sHeader({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="none"
|
||||
disabled={groupBy?.length > 0}
|
||||
data-testid="k8s-list-filters-button"
|
||||
onClick={(): void => setIsFiltersSidePanelOpen(true)}
|
||||
>
|
||||
@@ -217,7 +221,8 @@ function K8sHeader({
|
||||
|
||||
<K8sFiltersSidePanel
|
||||
open={isFiltersSidePanelOpen}
|
||||
entity={entity}
|
||||
columns={columns}
|
||||
storageKey={columnStorageKey}
|
||||
onClose={onClickOutside}
|
||||
/>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,15 +13,14 @@ export type K8sBaseFilters = {
|
||||
orderBy?: OrderBySchemaType;
|
||||
};
|
||||
|
||||
export type K8sRenderedRowData = {
|
||||
/**
|
||||
* The unique ID for the row
|
||||
*/
|
||||
/**
|
||||
* Type for table row data with required key fields.
|
||||
* Used when rendering raw data in the table.
|
||||
*/
|
||||
export type K8sTableRowData<T> = T & {
|
||||
key: string;
|
||||
/**
|
||||
* The ID to the selectedItem
|
||||
*/
|
||||
id: string;
|
||||
itemKey: string;
|
||||
groupedByMeta: Record<string, string>;
|
||||
[key: string]: unknown;
|
||||
/** Metadata about which attributes were used for grouping */
|
||||
groupedByMeta?: Record<string, string>;
|
||||
};
|
||||
|
||||
@@ -22,30 +22,32 @@ const dotToUnder: Record<string, string> = {
|
||||
'k8s.persistentvolumeclaim.name': 'k8s_persistentvolumeclaim_name',
|
||||
};
|
||||
|
||||
export function getGroupedByMeta<T extends { meta: Record<string, string> }>(
|
||||
export function getGroupedByMeta<T extends { meta?: Record<string, string> }>(
|
||||
itemData: T,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
const meta = itemData.meta ?? {};
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof itemData.meta;
|
||||
result[rawKey] = (itemData.meta[metaKey] || itemData.meta[rawKey]) ?? '';
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof meta;
|
||||
result[rawKey] = (meta[metaKey] || meta[rawKey]) ?? '';
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getRowKey<T extends { meta: Record<string, string> }>(
|
||||
export function getRowKey<T extends { meta?: Record<string, string> }>(
|
||||
itemData: T,
|
||||
getItemIdentifier: () => string,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): string {
|
||||
const nodeIdentifier = getItemIdentifier();
|
||||
const meta = itemData.meta ?? {};
|
||||
|
||||
if (groupBy.length === 0) {
|
||||
return nodeIdentifier || JSON.stringify(itemData.meta);
|
||||
return nodeIdentifier || JSON.stringify(meta);
|
||||
}
|
||||
|
||||
const groupedMeta = getGroupedByMeta(itemData, groupBy);
|
||||
@@ -61,30 +63,32 @@ export function getRowKey<T extends { meta: Record<string, string> }>(
|
||||
return nodeIdentifier;
|
||||
}
|
||||
|
||||
return JSON.stringify(itemData.meta);
|
||||
return JSON.stringify(meta);
|
||||
}
|
||||
|
||||
export function getGroupByEl<T extends { meta: Record<string, string> }>(
|
||||
export function getGroupByEl<T extends { meta?: Record<string, string> }>(
|
||||
itemData: T,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): React.ReactNode {
|
||||
const groupByValues: string[] = [];
|
||||
const meta = itemData.meta ?? {};
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
|
||||
// Choose mapped key if present, otherwise use rawKey
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof itemData.meta;
|
||||
const value = itemData.meta[metaKey] || itemData.meta[rawKey] || '<no-value>';
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof meta;
|
||||
const value = meta[metaKey] || meta[rawKey] || '<no-value>';
|
||||
|
||||
groupByValues.push(value);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.itemDataGroup}>
|
||||
{groupByValues.map((value) => (
|
||||
{groupByValues.map((value, index) => (
|
||||
<Badge
|
||||
key={value}
|
||||
// oxlint-disable-next-line react/no-array-index-key
|
||||
key={`${index}-${value}`}
|
||||
color="secondary"
|
||||
className={styles.itemDataGroupTagItem}
|
||||
>
|
||||
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
k8sClusterInitialLogTracesFilter,
|
||||
} from './constants';
|
||||
import {
|
||||
k8sClustersColumns,
|
||||
getK8sClusterItemKey,
|
||||
getK8sClusterRowKey,
|
||||
k8sClustersColumnsConfig,
|
||||
k8sClustersRenderRowData,
|
||||
} from './table.config';
|
||||
|
||||
function K8sClustersList({
|
||||
@@ -91,10 +91,10 @@ function K8sClustersList({
|
||||
<K8sBaseList<K8sClusterData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.CLUSTERS}
|
||||
tableColumnsDefinitions={k8sClustersColumns}
|
||||
tableColumns={k8sClustersColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sClustersRenderRowData}
|
||||
getRowKey={getK8sClusterRowKey}
|
||||
getItemKey={getK8sClusterItemKey}
|
||||
eventCategory={InfraMonitoringEvents.Cluster}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,77 +1,26 @@
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Tooltip } from 'antd';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8s/components';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/types';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import { formatBytes, ValidateColumnValueWrapper } from '../commonUtils';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { ValidateColumnValueWrapper } from '../components';
|
||||
import { K8sClusterData, K8sClustersListPayload } from './api';
|
||||
import { Boxes } from 'lucide-react';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export interface K8sClustersRowData {
|
||||
key: string;
|
||||
itemKey: string;
|
||||
clusterUID: string;
|
||||
clusterName: React.ReactNode;
|
||||
cpu: React.ReactNode;
|
||||
cpu_allocatable: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
memory_allocatable: React.ReactNode;
|
||||
groupedByMeta?: Record<string, string>;
|
||||
export function getK8sClusterRowKey(cluster: K8sClusterData): string {
|
||||
return (
|
||||
cluster.clusterUID ||
|
||||
cluster.meta.k8s_cluster_uid ||
|
||||
cluster.meta.k8s_cluster_name
|
||||
);
|
||||
}
|
||||
|
||||
export const k8sClustersColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Cluster Group',
|
||||
value: 'clusterGroup',
|
||||
id: 'clusterGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
value: 'clusterName',
|
||||
id: 'clusterName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Alloc (cores)',
|
||||
value: 'cpu_allocatable',
|
||||
id: 'cpu_allocatable',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Memory Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Memory Alloc (bytes)',
|
||||
value: 'memory_allocatable',
|
||||
id: 'memory_allocatable',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
export function getK8sClusterItemKey(cluster: K8sClusterData): string {
|
||||
return cluster.meta.k8s_cluster_name;
|
||||
}
|
||||
|
||||
export const getK8sClustersListQuery = (): K8sClustersListPayload => ({
|
||||
filters: {
|
||||
@@ -81,103 +30,110 @@ export const getK8sClustersListQuery = (): K8sClustersListPayload => ({
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
export const k8sClustersColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
export const k8sClustersColumnsConfig: TableColumnDef<K8sClusterData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> CLUSTER GROUP
|
||||
</div>
|
||||
id: 'clusterGroup',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="CLUSTER GROUP" />,
|
||||
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
|
||||
width: { min: 300 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-collapse',
|
||||
cell: ({ isExpanded, toggleExpanded, row }): JSX.Element | null => {
|
||||
return (
|
||||
<ExpandButtonWrapper
|
||||
isExpanded={isExpanded}
|
||||
toggleExpanded={toggleExpanded}
|
||||
>
|
||||
<K8sGroupCell row={row} />
|
||||
</ExpandButtonWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'clusterName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Cluster Name"
|
||||
icon={<Boxes data-hide-expanded="true" size={14} />}
|
||||
/>
|
||||
),
|
||||
dataIndex: 'clusterGroup',
|
||||
key: 'clusterGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const clusterName = value as string;
|
||||
return (
|
||||
<Tooltip title={clusterName}>
|
||||
<TanStackTable.Text>{clusterName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Cluster Name</div>,
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
accessorFn: (row): number => row.cpuUsage,
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={cpu}>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'cpu_allocatable',
|
||||
header: 'CPU Alloc (cores)',
|
||||
accessorFn: (row): number => row.cpuAllocatable,
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuAllocatable = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={cpuAllocatable}>
|
||||
<TanStackTable.Text>{cpuAllocatable}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>CPU Alloc (cores)</div>,
|
||||
dataIndex: 'cpu_allocatable',
|
||||
key: 'cpu_allocatable',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'memory',
|
||||
header: 'Memory Usage (WSS)',
|
||||
accessorFn: (row): number => row.memoryUsage,
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={memory}>
|
||||
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Memory Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Memory Allocatable</div>,
|
||||
dataIndex: 'memory_allocatable',
|
||||
key: 'memory_allocatable',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'memory_allocatable',
|
||||
header: 'Memory Allocatable',
|
||||
accessorFn: (row): number => row.memoryAllocatable,
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryAllocatable = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={memoryAllocatable}>
|
||||
<TanStackTable.Text>{formatBytes(memoryAllocatable)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sClustersRenderRowData = (
|
||||
cluster: K8sClusterData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(
|
||||
cluster,
|
||||
() =>
|
||||
cluster.clusterUID ||
|
||||
cluster.meta.k8s_cluster_uid ||
|
||||
cluster.meta.k8s_cluster_name,
|
||||
groupBy,
|
||||
),
|
||||
itemKey: cluster.meta.k8s_cluster_name,
|
||||
clusterUID: cluster.clusterUID || cluster.meta.k8s_cluster_uid,
|
||||
clusterName: (
|
||||
<Tooltip title={cluster.meta.k8s_cluster_name}>
|
||||
{cluster.meta.k8s_cluster_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={cluster.cpuUsage}>
|
||||
{cluster.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={cluster.memoryUsage}>
|
||||
{formatBytes(cluster.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_allocatable: (
|
||||
<ValidateColumnValueWrapper value={cluster.cpuAllocatable}>
|
||||
{cluster.cpuAllocatable}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_allocatable: (
|
||||
<ValidateColumnValueWrapper value={cluster.memoryAllocatable}>
|
||||
{formatBytes(cluster.memoryAllocatable)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
clusterGroup: getGroupByEl(cluster, groupBy),
|
||||
...cluster.meta,
|
||||
groupedByMeta: getGroupedByMeta(cluster, groupBy),
|
||||
});
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
.entityGroupHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
}
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
k8sDaemonSetInitialLogTracesFilter,
|
||||
} from './constants';
|
||||
import {
|
||||
k8sDaemonSetsColumns,
|
||||
getK8sDaemonSetItemKey,
|
||||
getK8sDaemonSetRowKey,
|
||||
k8sDaemonSetsColumnsConfig,
|
||||
k8sDaemonSetsRenderRowData,
|
||||
} from './table.config';
|
||||
|
||||
function K8sDaemonSetsList({
|
||||
@@ -91,10 +91,10 @@ function K8sDaemonSetsList({
|
||||
<K8sBaseList<K8sDaemonSetsData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
tableColumnsDefinitions={k8sDaemonSetsColumns}
|
||||
tableColumns={k8sDaemonSetsColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sDaemonSetsRenderRowData}
|
||||
getRowKey={getK8sDaemonSetRowKey}
|
||||
getItemKey={getK8sDaemonSetItemKey}
|
||||
eventCategory={InfraMonitoringEvents.DaemonSet}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,297 +1,225 @@
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Tooltip } from 'antd';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8s/components';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/types';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
formatBytes,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../commonUtils';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { K8sDaemonSetsData } from './api';
|
||||
import { Group } from 'lucide-react';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
export function getK8sDaemonSetRowKey(daemonSet: K8sDaemonSetsData): string {
|
||||
return (
|
||||
daemonSet.daemonSetName ||
|
||||
daemonSet.meta.k8s_daemonset_name ||
|
||||
`${daemonSet.meta.k8s_namespace_name}-${daemonSet.meta.k8s_daemonset_name}`
|
||||
);
|
||||
}
|
||||
|
||||
export const k8sDaemonSetsColumns: IEntityColumn[] = [
|
||||
export function getK8sDaemonSetItemKey(daemonSet: K8sDaemonSetsData): string {
|
||||
return daemonSet.meta.k8s_daemonset_name;
|
||||
}
|
||||
|
||||
export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
|
||||
{
|
||||
label: 'DaemonSet Group',
|
||||
value: 'daemonSetGroup',
|
||||
id: 'daemonSetGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="DAEMONSET GROUP" />,
|
||||
accessorFn: (row): string => row.meta.k8s_daemonset_name || '',
|
||||
width: { min: 300 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-collapse',
|
||||
cell: ({ isExpanded, toggleExpanded, row }): JSX.Element | null => {
|
||||
return (
|
||||
<ExpandButtonWrapper
|
||||
isExpanded={isExpanded}
|
||||
toggleExpanded={toggleExpanded}
|
||||
>
|
||||
<K8sGroupCell row={row} />
|
||||
</ExpandButtonWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'DaemonSet Name',
|
||||
value: 'daemonsetName',
|
||||
id: 'daemonsetName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Available',
|
||||
value: 'available_nodes',
|
||||
id: 'available_nodes',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Desired',
|
||||
value: 'desired_nodes',
|
||||
id: 'desired_nodes',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Req Usage (%)',
|
||||
value: 'cpu_request',
|
||||
id: 'cpu_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Limit Usage (%)',
|
||||
value: 'cpu_limit',
|
||||
id: 'cpu_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Req Usage (%)',
|
||||
value: 'memory_request',
|
||||
id: 'memory_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Limit Usage (%)',
|
||||
value: 'memory_limit',
|
||||
id: 'memory_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDaemonSetsColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> DAEMONSET GROUP
|
||||
</div>
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="DaemonSet Name"
|
||||
icon={<Group data-hide-expanded="true" size={14} />}
|
||||
/>
|
||||
),
|
||||
dataIndex: 'daemonSetGroup',
|
||||
key: 'daemonSetGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
accessorFn: (row): string => row.meta.k8s_daemonset_name || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const daemonsetName = value as string;
|
||||
return (
|
||||
<Tooltip title={daemonsetName}>
|
||||
<TanStackTable.Text>{daemonsetName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>DaemonSet Name</div>,
|
||||
dataIndex: 'daemonsetName',
|
||||
key: 'daemonsetName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
id: 'namespaceName',
|
||||
header: 'Namespace Name',
|
||||
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
|
||||
width: { default: 100 },
|
||||
enableSort: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const namespaceName = value as string;
|
||||
return (
|
||||
<Tooltip title={namespaceName}>
|
||||
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Namespace Name</div>,
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
id: 'available_nodes',
|
||||
header: 'Available',
|
||||
accessorFn: (row): number => row.availableNodes,
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const availableNodes = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={availableNodes}>
|
||||
<TanStackTable.Text>{availableNodes}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Available</div>,
|
||||
dataIndex: 'available_nodes',
|
||||
key: 'available_nodes',
|
||||
width: 50,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'desired_nodes',
|
||||
header: 'Desired',
|
||||
accessorFn: (row): number => row.desiredNodes,
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const desiredNodes = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={desiredNodes}>
|
||||
<TanStackTable.Text>{desiredNodes}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Desired</div>,
|
||||
dataIndex: 'desired_nodes',
|
||||
key: 'desired_nodes',
|
||||
width: 50,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'cpu_request',
|
||||
header: 'CPU Req Usage (%)',
|
||||
accessorFn: (row): number => row.cpuRequest,
|
||||
width: { min: 200, default: 200 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuRequest}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>CPU Req Usage (%)</div>,
|
||||
dataIndex: 'cpu_request',
|
||||
key: 'cpu_request',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'cpu_limit',
|
||||
header: 'CPU Limit Usage (%)',
|
||||
accessorFn: (row): number => row.cpuLimit,
|
||||
width: { min: 200, default: 200 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuLimit = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuLimit}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>CPU Limit Usage (%)</div>,
|
||||
dataIndex: 'cpu_limit',
|
||||
key: 'cpu_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
accessorFn: (row): number => row.cpuUsage,
|
||||
width: { min: 190 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={cpu}>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'memory_request',
|
||||
header: 'Mem Req Usage (%)',
|
||||
accessorFn: (row): number => row.memoryRequest,
|
||||
width: { min: 190 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memoryRequest}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Mem Req Usage (%)</div>,
|
||||
dataIndex: 'memory_request',
|
||||
key: 'memory_request',
|
||||
width: 170,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'memory_limit',
|
||||
header: 'Mem Limit Usage (%)',
|
||||
accessorFn: (row): number => row.memoryLimit,
|
||||
width: { min: 180 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryLimit = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memoryLimit}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Mem Limit Usage (%)</div>,
|
||||
dataIndex: 'memory_limit',
|
||||
key: 'memory_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'memory',
|
||||
header: 'Mem Usage (WSS)',
|
||||
accessorFn: (row): number => row.memoryUsage,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={memory}>
|
||||
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDaemonSetsRenderRowData = (
|
||||
entity: K8sDaemonSetsData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(
|
||||
entity,
|
||||
() => entity.daemonSetName || entity.meta.k8s_daemonset_name || '',
|
||||
groupBy,
|
||||
),
|
||||
itemKey: entity.meta.k8s_daemonset_name,
|
||||
daemonsetName: (
|
||||
<Tooltip title={entity.meta.k8s_daemonset_name}>
|
||||
{entity.meta.k8s_daemonset_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
namespaceName: (
|
||||
<Tooltip title={entity.meta.k8s_namespace_name}>
|
||||
{entity.meta.k8s_namespace_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={entity.cpuRequest}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={entity.cpuRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={entity.cpuLimit}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={entity.cpuLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={entity.cpuUsage}>
|
||||
{entity.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={entity.memoryRequest}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={entity.memoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={entity.memoryLimit}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={entity.memoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={entity.memoryUsage}>
|
||||
{formatBytes(entity.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
available_nodes: (
|
||||
<ValidateColumnValueWrapper value={entity.availableNodes}>
|
||||
{entity.availableNodes}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
desired_nodes: (
|
||||
<ValidateColumnValueWrapper value={entity.desiredNodes}>
|
||||
{entity.desiredNodes}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
daemonSetGroup: getGroupByEl(entity, groupBy),
|
||||
...entity.meta,
|
||||
groupedByMeta: getGroupedByMeta(entity, groupBy),
|
||||
});
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.entityGroupHeader {
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
k8sDeploymentInitialLogTracesFilter,
|
||||
} from './constants';
|
||||
import {
|
||||
k8sDeploymentsColumns,
|
||||
getK8sDeploymentItemKey,
|
||||
getK8sDeploymentRowKey,
|
||||
k8sDeploymentsColumnsConfig,
|
||||
k8sDeploymentsRenderRowData,
|
||||
} from './table.config';
|
||||
|
||||
function K8sDeploymentsList({
|
||||
@@ -91,10 +91,10 @@ function K8sDeploymentsList({
|
||||
<K8sBaseList<K8sDeploymentsData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
tableColumnsDefinitions={k8sDeploymentsColumns}
|
||||
tableColumns={k8sDeploymentsColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sDeploymentsRenderRowData}
|
||||
getRowKey={getK8sDeploymentRowKey}
|
||||
getItemKey={getK8sDeploymentItemKey}
|
||||
eventCategory={InfraMonitoringEvents.Deployment}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,269 +1,230 @@
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Tooltip } from 'antd';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8s/components';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/types';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
formatBytes,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../commonUtils';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { K8sDeploymentsData } from './api';
|
||||
import { Computer } from 'lucide-react';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
export function getK8sDeploymentRowKey(deployment: K8sDeploymentsData): string {
|
||||
return deployment.meta.k8s_deployment_name || deployment.deploymentName;
|
||||
}
|
||||
|
||||
export const k8sDeploymentsColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Deployment Group',
|
||||
value: 'deploymentGroup',
|
||||
id: 'deploymentGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'Deployment Name',
|
||||
value: 'deploymentName',
|
||||
id: 'deploymentName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Available',
|
||||
value: 'available_pods',
|
||||
id: 'available_pods',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Desired',
|
||||
value: 'desired_pods',
|
||||
id: 'desired_pods',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Req Usage (%)',
|
||||
value: 'cpu_request',
|
||||
id: 'cpu_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Limit Usage (%)',
|
||||
value: 'cpu_limit',
|
||||
id: 'cpu_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Req Usage (%)',
|
||||
value: 'memory_request',
|
||||
id: 'memory_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Limit Usage (%)',
|
||||
value: 'memory_limit',
|
||||
id: 'memory_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDeploymentsColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> DEPLOYMENT GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'deploymentGroup',
|
||||
key: 'deploymentGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
},
|
||||
{
|
||||
title: <div>Deployment Name</div>,
|
||||
dataIndex: 'deploymentName',
|
||||
key: 'deploymentName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Namespace Name</div>,
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Available</div>,
|
||||
dataIndex: 'available_pods',
|
||||
key: 'available_pods',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Desired</div>,
|
||||
dataIndex: 'desired_pods',
|
||||
key: 'desired_pods',
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Req Usage (%)</div>,
|
||||
dataIndex: 'cpu_request',
|
||||
key: 'cpu_request',
|
||||
width: 170,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Limit Usage (%)</div>,
|
||||
dataIndex: 'cpu_limit',
|
||||
key: 'cpu_limit',
|
||||
width: 170,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Req Usage (%)</div>,
|
||||
dataIndex: 'memory_request',
|
||||
key: 'memory_request',
|
||||
width: 170,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Limit Usage (%)</div>,
|
||||
dataIndex: 'memory_limit',
|
||||
key: 'memory_limit',
|
||||
width: 170,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDeploymentsRenderRowData = (
|
||||
export function getK8sDeploymentItemKey(
|
||||
deployment: K8sDeploymentsData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(deployment, () => deployment.meta.k8s_deployment_name, groupBy),
|
||||
itemKey: deployment.meta.k8s_deployment_name,
|
||||
deploymentName: (
|
||||
<Tooltip title={deployment.meta.k8s_deployment_name}>
|
||||
{deployment.meta.k8s_deployment_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
namespaceName: deployment.meta.k8s_namespace_name,
|
||||
available_pods: (
|
||||
<ValidateColumnValueWrapper value={deployment.availablePods}>
|
||||
{deployment.availablePods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
desired_pods: (
|
||||
<ValidateColumnValueWrapper value={deployment.desiredPods}>
|
||||
{deployment.desiredPods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={deployment.cpuUsage}>
|
||||
{deployment.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper value={deployment.cpuRequest}>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={deployment.cpuRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper value={deployment.cpuLimit}>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={deployment.cpuLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={deployment.memoryUsage}>
|
||||
{formatBytes(deployment.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper value={deployment.memoryRequest}>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={deployment.memoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper value={deployment.memoryLimit}>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={deployment.memoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
deploymentGroup: getGroupByEl(deployment, groupBy),
|
||||
...deployment.meta,
|
||||
groupedByMeta: getGroupedByMeta(deployment, groupBy),
|
||||
});
|
||||
): string {
|
||||
return deployment.meta.k8s_deployment_name;
|
||||
}
|
||||
|
||||
export const k8sDeploymentsColumnsConfig: TableColumnDef<K8sDeploymentsData>[] =
|
||||
[
|
||||
{
|
||||
id: 'deploymentGroup',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader title="DEPLOYMENT GROUP" />
|
||||
),
|
||||
accessorFn: (row): string => row.meta.k8s_deployment_name || '',
|
||||
width: { min: 220 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-collapse',
|
||||
cell: ({ isExpanded, toggleExpanded, row }): JSX.Element | null => {
|
||||
return (
|
||||
<ExpandButtonWrapper
|
||||
isExpanded={isExpanded}
|
||||
toggleExpanded={toggleExpanded}
|
||||
>
|
||||
<K8sGroupCell row={row} />
|
||||
</ExpandButtonWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'deploymentName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Deployment Name"
|
||||
icon={<Computer data-hide-expanded="true" size={14} />}
|
||||
/>
|
||||
),
|
||||
accessorFn: (row): string => row.meta.k8s_deployment_name || '',
|
||||
width: { min: 210 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const deploymentName = value as string;
|
||||
return (
|
||||
<Tooltip title={deploymentName}>
|
||||
<TanStackTable.Text>{deploymentName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'namespaceName',
|
||||
header: 'Namespace Name',
|
||||
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
|
||||
width: { default: 220 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
<TanStackTable.Text>{value as string}</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'available_pods',
|
||||
header: 'Available',
|
||||
accessorFn: (row): number => row.availablePods,
|
||||
width: { min: 100 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const availablePods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={availablePods}>
|
||||
<TanStackTable.Text>{availablePods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'desired_pods',
|
||||
header: 'Desired',
|
||||
accessorFn: (row): number => row.desiredPods,
|
||||
width: { min: 80 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const desiredPods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={desiredPods}>
|
||||
<TanStackTable.Text>{desiredPods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: 'CPU Req Usage (%)',
|
||||
accessorFn: (row): number => row.cpuRequest,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuRequest}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: 'CPU Limit Usage (%)',
|
||||
accessorFn: (row): number => row.cpuLimit,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuLimit = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuLimit}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
accessorFn: (row): number => row.cpuUsage,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={cpu}>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: 'Mem Req Usage (%)',
|
||||
accessorFn: (row): number => row.memoryRequest,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memoryRequest}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: 'Mem Limit Usage (%)',
|
||||
accessorFn: (row): number => row.memoryLimit,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryLimit = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memoryLimit}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
header: 'Mem Usage (WSS)',
|
||||
accessorFn: (row): number => row.memoryUsage,
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={memory}>
|
||||
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.entityGroupHeader {
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
@@ -43,7 +43,7 @@ import K8sDaemonSetsList from './DaemonSets/K8sDaemonSetsList';
|
||||
import K8sDeploymentsList from './Deployments/K8sDeploymentsList';
|
||||
import {
|
||||
useInfraMonitoringCategory,
|
||||
useInfraMonitoringFilters,
|
||||
useInfraMonitoringFiltersK8s,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
} from './hooks';
|
||||
@@ -60,7 +60,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useInfraMonitoringCategory();
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFilters();
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
|
||||
const [, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
k8sJobInitialLogTracesFilter,
|
||||
} from './constants';
|
||||
import {
|
||||
k8sJobsColumns,
|
||||
getK8sJobItemKey,
|
||||
getK8sJobRowKey,
|
||||
k8sJobsColumnsConfig,
|
||||
k8sJobsRenderRowData,
|
||||
} from './table.config';
|
||||
|
||||
function K8sJobsList({
|
||||
@@ -91,10 +91,10 @@ function K8sJobsList({
|
||||
<K8sBaseList<K8sJobsData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
tableColumnsDefinitions={k8sJobsColumns}
|
||||
tableColumns={k8sJobsColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sJobsRenderRowData}
|
||||
getRowKey={getK8sJobRowKey}
|
||||
getItemKey={getK8sJobItemKey}
|
||||
eventCategory={InfraMonitoringEvents.Job}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,330 +1,249 @@
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Tooltip } from 'antd';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8s/components';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/types';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
formatBytes,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../commonUtils';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { K8sJobsData } from './api';
|
||||
import { Bolt } from 'lucide-react';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
export function getK8sJobRowKey(job: K8sJobsData): string {
|
||||
return job.jobName || job.meta.k8s_job_name || '';
|
||||
}
|
||||
|
||||
export const k8sJobsColumns: IEntityColumn[] = [
|
||||
export function getK8sJobItemKey(job: K8sJobsData): string {
|
||||
return job.meta.k8s_job_name;
|
||||
}
|
||||
|
||||
export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
|
||||
{
|
||||
label: 'Job Group',
|
||||
value: 'jobGroup',
|
||||
id: 'jobGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="JOB GROUP" />,
|
||||
accessorFn: (row): string => row.meta.k8s_job_name || '',
|
||||
width: { min: 270 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-collapse',
|
||||
cell: ({ isExpanded, toggleExpanded, row }): JSX.Element | null => {
|
||||
return (
|
||||
<ExpandButtonWrapper
|
||||
isExpanded={isExpanded}
|
||||
toggleExpanded={toggleExpanded}
|
||||
>
|
||||
<K8sGroupCell row={row} />
|
||||
</ExpandButtonWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Job Name',
|
||||
value: 'jobName',
|
||||
id: 'jobName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Successful',
|
||||
value: 'successful_pods',
|
||||
id: 'successful_pods',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Failed',
|
||||
value: 'failed_pods',
|
||||
id: 'failed_pods',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Desired Successful',
|
||||
value: 'desired_successful_pods',
|
||||
id: 'desired_successful_pods',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Active',
|
||||
value: 'active_pods',
|
||||
id: 'active_pods',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Req Usage (%)',
|
||||
value: 'cpu_request',
|
||||
id: 'cpu_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Limit Usage (%)',
|
||||
value: 'cpu_limit',
|
||||
id: 'cpu_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Req Usage (%)',
|
||||
value: 'memory_request',
|
||||
id: 'memory_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Limit Usage (%)',
|
||||
value: 'memory_limit',
|
||||
id: 'memory_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sJobsColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> JOB GROUP
|
||||
</div>
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Job Name"
|
||||
icon={<Bolt data-hide-expanded="true" size={14} />}
|
||||
/>
|
||||
),
|
||||
dataIndex: 'jobGroup',
|
||||
key: 'jobGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
accessorFn: (row): string => row.meta.k8s_job_name || '',
|
||||
width: { min: 260 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const jobName = value as string;
|
||||
return (
|
||||
<Tooltip title={jobName}>
|
||||
<TanStackTable.Text>{jobName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Job Name</div>,
|
||||
dataIndex: 'jobName',
|
||||
key: 'jobName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
id: 'namespaceName',
|
||||
header: 'Namespace Name',
|
||||
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
|
||||
width: { default: 150 },
|
||||
enableSort: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const namespaceName = value as string;
|
||||
return (
|
||||
<Tooltip title={namespaceName}>
|
||||
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Namespace Name</div>,
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
id: 'successful_pods',
|
||||
header: 'Successful',
|
||||
accessorFn: (row): number => row.successfulPods,
|
||||
width: { min: 120 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const successfulPods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={successfulPods}>
|
||||
<TanStackTable.Text>{successfulPods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Successful</div>,
|
||||
dataIndex: 'successful_pods',
|
||||
key: 'successful_pods',
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'failed_pods',
|
||||
header: 'Failed',
|
||||
accessorFn: (row): number => row.failedPods,
|
||||
width: { min: 100 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const failedPods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={failedPods}>
|
||||
<TanStackTable.Text>{failedPods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Failed</div>,
|
||||
dataIndex: 'failed_pods',
|
||||
key: 'failed_pods',
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'desired_successful_pods',
|
||||
header: 'Desired Successful',
|
||||
accessorFn: (row): number => row.desiredSuccessfulPods,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const desiredSuccessfulPods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={desiredSuccessfulPods}>
|
||||
<TanStackTable.Text>{desiredSuccessfulPods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Desired Successful</div>,
|
||||
dataIndex: 'desired_successful_pods',
|
||||
key: 'desired_successful_pods',
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'active_pods',
|
||||
header: 'Active',
|
||||
accessorFn: (row): number => row.activePods,
|
||||
width: { min: 100 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const activePods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={activePods}>
|
||||
<TanStackTable.Text>{activePods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Active</div>,
|
||||
dataIndex: 'active_pods',
|
||||
key: 'active_pods',
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'cpu_request',
|
||||
header: 'CPU Req Usage (%)',
|
||||
accessorFn: (row): number => row.cpuRequest,
|
||||
width: { min: 200, default: 200 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuRequest}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>CPU Req Usage (%)</div>,
|
||||
dataIndex: 'cpu_request',
|
||||
key: 'cpu_request',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'cpu_limit',
|
||||
header: 'CPU Limit Usage (%)',
|
||||
accessorFn: (row): number => row.cpuLimit,
|
||||
width: { min: 200, default: 200 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuLimit = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuLimit}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>CPU Limit Usage (%)</div>,
|
||||
dataIndex: 'cpu_limit',
|
||||
key: 'cpu_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
accessorFn: (row): number => row.cpuUsage,
|
||||
width: { min: 190 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={cpu}>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'memory_request',
|
||||
header: 'Mem Req Usage (%)',
|
||||
accessorFn: (row): number => row.memoryRequest,
|
||||
width: { min: 190 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memoryRequest}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Mem Req Usage (%)</div>,
|
||||
dataIndex: 'memory_request',
|
||||
key: 'memory_request',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'memory_limit',
|
||||
header: 'Mem Limit Usage (%)',
|
||||
accessorFn: (row): number => row.memoryLimit,
|
||||
width: { min: 180 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryLimit = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memoryLimit}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Mem Limit Usage (%)</div>,
|
||||
dataIndex: 'memory_limit',
|
||||
key: 'memory_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'memory',
|
||||
header: 'Mem Usage (WSS)',
|
||||
accessorFn: (row): number => row.memoryUsage,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={memory}>
|
||||
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sJobsRenderRowData = (
|
||||
job: K8sJobsData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(job, () => job.jobName || job.meta.k8s_job_name || '', groupBy),
|
||||
itemKey: job.meta.k8s_job_name,
|
||||
jobName: (
|
||||
<Tooltip title={job.meta.k8s_job_name}>{job.meta.k8s_job_name || ''}</Tooltip>
|
||||
),
|
||||
namespaceName: (
|
||||
<Tooltip title={job.meta.k8s_namespace_name}>
|
||||
{job.meta.k8s_namespace_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={job.cpuRequest}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={job.cpuRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={job.cpuLimit}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={job.cpuLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={job.cpuUsage}>
|
||||
{job.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={job.memoryRequest}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={job.memoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={job.memoryLimit}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={job.memoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={job.memoryUsage}>
|
||||
{formatBytes(job.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
successful_pods: (
|
||||
<ValidateColumnValueWrapper value={job.successfulPods}>
|
||||
{job.successfulPods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
desired_successful_pods: (
|
||||
<ValidateColumnValueWrapper value={job.desiredSuccessfulPods}>
|
||||
{job.desiredSuccessfulPods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
failed_pods: (
|
||||
<ValidateColumnValueWrapper value={job.failedPods}>
|
||||
{job.failedPods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
active_pods: (
|
||||
<ValidateColumnValueWrapper value={job.activePods}>
|
||||
{job.activePods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
jobGroup: getGroupByEl(job, groupBy),
|
||||
...job.meta,
|
||||
groupedByMeta: getGroupedByMeta(job, groupBy),
|
||||
});
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.entityGroupHeader {
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
namespaceWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
k8sNamespacesColumns,
|
||||
getK8sNamespaceItemKey,
|
||||
getK8sNamespaceRowKey,
|
||||
k8sNamespacesColumnsConfig,
|
||||
k8sNamespacesRenderRowData,
|
||||
} from './table.config';
|
||||
|
||||
function K8sNamespacesList({
|
||||
@@ -91,10 +91,10 @@ function K8sNamespacesList({
|
||||
<K8sBaseList<K8sNamespacesData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.NAMESPACES}
|
||||
tableColumnsDefinitions={k8sNamespacesColumns}
|
||||
tableColumns={k8sNamespacesColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sNamespacesRenderRowData}
|
||||
getRowKey={getK8sNamespaceRowKey}
|
||||
getItemKey={getK8sNamespaceItemKey}
|
||||
eventCategory={InfraMonitoringEvents.Namespace}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,68 +1,21 @@
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Tooltip } from 'antd';
|
||||
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8s/components';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/types';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import { formatBytes, ValidateColumnValueWrapper } from '../commonUtils';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { ValidateColumnValueWrapper } from '../components';
|
||||
import { K8sNamespacesData, K8sNamespacesListPayload } from './api';
|
||||
import { FilePenLine } from 'lucide-react';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export interface K8sNamespacesRowData {
|
||||
key: string;
|
||||
itemKey: string;
|
||||
namespaceUID: string;
|
||||
namespaceName: React.ReactNode;
|
||||
clusterName: string;
|
||||
cpu: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
groupedByMeta?: Record<string, string>;
|
||||
export function getK8sNamespaceRowKey(namespace: K8sNamespacesData): string {
|
||||
return namespace.namespaceName || namespace.meta.k8s_namespace_name;
|
||||
}
|
||||
|
||||
export const k8sNamespacesColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Namespace Group',
|
||||
value: 'namespaceGroup',
|
||||
id: 'namespaceGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
value: 'clusterName',
|
||||
id: 'clusterName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Memory Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
export function getK8sNamespaceItemKey(namespace: K8sNamespacesData): string {
|
||||
return namespace.meta.k8s_namespace_name;
|
||||
}
|
||||
|
||||
export const getK8sNamespacesListQuery = (): K8sNamespacesListPayload => ({
|
||||
filters: {
|
||||
@@ -72,84 +25,90 @@ export const getK8sNamespacesListQuery = (): K8sNamespacesListPayload => ({
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
export const k8sNamespacesColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
export const k8sNamespacesColumnsConfig: TableColumnDef<K8sNamespacesData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> NAMESPACE GROUP
|
||||
</div>
|
||||
id: 'namespaceGroup',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="NAMESPACE GROUP" />,
|
||||
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
|
||||
width: { min: 300 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-collapse',
|
||||
cell: ({ isExpanded, toggleExpanded, row }): JSX.Element | null => {
|
||||
return (
|
||||
<ExpandButtonWrapper
|
||||
isExpanded={isExpanded}
|
||||
toggleExpanded={toggleExpanded}
|
||||
>
|
||||
<K8sGroupCell row={row} />
|
||||
</ExpandButtonWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'namespaceName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Namespace Name"
|
||||
icon={<FilePenLine data-hide-expanded="true" size={14} />}
|
||||
/>
|
||||
),
|
||||
dataIndex: 'namespaceGroup',
|
||||
key: 'namespaceGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
accessorFn: (row): string => row.namespaceName || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const namespaceName = value as string;
|
||||
return (
|
||||
<Tooltip title={namespaceName}>
|
||||
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Namespace Name</div>,
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
id: 'clusterName',
|
||||
header: 'Cluster Name',
|
||||
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
|
||||
width: { default: 150 },
|
||||
enableSort: false,
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
<TanStackTable.Text>{value as string}</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <div>Cluster Name</div>,
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
accessorFn: (row): number => row.cpuUsage,
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={cpu}>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'memory',
|
||||
header: 'Mem Usage (WSS)',
|
||||
accessorFn: (row): number => row.memoryUsage,
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={memory}>
|
||||
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sNamespacesRenderRowData = (
|
||||
namespace: K8sNamespacesData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(
|
||||
namespace,
|
||||
() => namespace.namespaceName || namespace.meta.k8s_namespace_name,
|
||||
groupBy,
|
||||
),
|
||||
itemKey: namespace.meta.k8s_namespace_name,
|
||||
namespaceUID: namespace.namespaceName,
|
||||
namespaceName: (
|
||||
<Tooltip title={namespace.namespaceName}>
|
||||
{namespace.namespaceName || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
clusterName: namespace.meta.k8s_cluster_name,
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={namespace.cpuUsage}>
|
||||
{namespace.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={namespace.memoryUsage}>
|
||||
{formatBytes(namespace.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
namespaceGroup: getGroupByEl(namespace, groupBy),
|
||||
...namespace.meta,
|
||||
groupedByMeta: getGroupedByMeta(namespace, groupBy),
|
||||
});
|
||||
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
nodeWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
k8sNodesColumns,
|
||||
getK8sNodeItemKey,
|
||||
getK8sNodeRowKey,
|
||||
k8sNodesColumnsConfig,
|
||||
k8sNodesRenderRowData,
|
||||
} from './table.config';
|
||||
|
||||
function K8sNodesList({
|
||||
@@ -91,10 +91,10 @@ function K8sNodesList({
|
||||
<K8sBaseList<K8sNodeData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.NODES}
|
||||
tableColumnsDefinitions={k8sNodesColumns}
|
||||
tableColumns={k8sNodesColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sNodesRenderRowData}
|
||||
getRowKey={getK8sNodeRowKey}
|
||||
getItemKey={getK8sNodeItemKey}
|
||||
eventCategory={InfraMonitoringEvents.Node}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,86 +1,22 @@
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Tooltip } from 'antd';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8s/components';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/types';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import { formatBytes, ValidateColumnValueWrapper } from '../commonUtils';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { ValidateColumnValueWrapper } from '../components';
|
||||
import { K8sNodeData, K8sNodesListPayload } from './api';
|
||||
import { Workflow } from 'lucide-react';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export interface K8sNodesRowData {
|
||||
key: string;
|
||||
itemKey: string;
|
||||
nodeUID: string;
|
||||
nodeName: React.ReactNode;
|
||||
clusterName: string;
|
||||
cpu: React.ReactNode;
|
||||
cpu_allocatable: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
memory_allocatable: React.ReactNode;
|
||||
groupedByMeta?: any;
|
||||
export function getK8sNodeRowKey(node: K8sNodeData): string {
|
||||
return node.nodeUID || node.meta.k8s_node_uid || node.meta.k8s_node_name;
|
||||
}
|
||||
|
||||
export const k8sNodesColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Node Group',
|
||||
value: 'nodeGroup',
|
||||
id: 'nodeGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'Node Name',
|
||||
value: 'nodeName',
|
||||
id: 'nodeName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
value: 'clusterName',
|
||||
id: 'clusterName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Alloc (cores)',
|
||||
value: 'cpu_allocatable',
|
||||
id: 'cpu_allocatable',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Memory Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Memory Alloc (bytes)',
|
||||
value: 'memory_allocatable',
|
||||
id: 'memory_allocatable',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
export function getK8sNodeItemKey(node: K8sNodeData): string {
|
||||
return node.meta.k8s_node_name;
|
||||
}
|
||||
|
||||
export const getK8sNodesListQuery = (): K8sNodesListPayload => ({
|
||||
filters: {
|
||||
@@ -90,110 +26,120 @@ export const getK8sNodesListQuery = (): K8sNodesListPayload => ({
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
export const k8sNodesColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
export const k8sNodesColumnsConfig: TableColumnDef<K8sNodeData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> NODE GROUP
|
||||
</div>
|
||||
id: 'nodeGroup',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="NODE GROUP" />,
|
||||
accessorFn: (row): string => row.meta.k8s_node_name || '',
|
||||
width: { min: 300 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-collapse',
|
||||
cell: ({ isExpanded, toggleExpanded, row }): JSX.Element | null => {
|
||||
return (
|
||||
<ExpandButtonWrapper
|
||||
isExpanded={isExpanded}
|
||||
toggleExpanded={toggleExpanded}
|
||||
>
|
||||
<K8sGroupCell row={row} />
|
||||
</ExpandButtonWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'nodeName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Node Name"
|
||||
icon={<Workflow data-hide-expanded="true" size={14} />}
|
||||
/>
|
||||
),
|
||||
dataIndex: 'nodeGroup',
|
||||
key: 'nodeGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
accessorFn: (row): string => row.meta.k8s_node_name || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const nodeName = value as string;
|
||||
return (
|
||||
<Tooltip title={nodeName}>
|
||||
<TanStackTable.Text>{nodeName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Node Name</div>,
|
||||
dataIndex: 'nodeName',
|
||||
key: 'nodeName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
id: 'clusterName',
|
||||
header: 'Cluster Name',
|
||||
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
|
||||
width: { min: 150, default: 150 },
|
||||
enableSort: false,
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
<TanStackTable.Text>{value as string}</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <div>Cluster Name</div>,
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
accessorFn: (row): number => row.nodeCPUUsage,
|
||||
width: { min: 200, default: 200 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={cpu}>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'cpu_allocatable',
|
||||
header: 'CPU Alloc (cores)',
|
||||
accessorFn: (row): number => row.nodeCPUAllocatable,
|
||||
width: { min: 200, default: 200 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuAllocatable = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={cpuAllocatable}>
|
||||
<TanStackTable.Text>{cpuAllocatable}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>CPU Alloc (cores)</div>,
|
||||
dataIndex: 'cpu_allocatable',
|
||||
key: 'cpu_allocatable',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'memory',
|
||||
header: 'Memory Usage (WSS)',
|
||||
accessorFn: (row): number => row.nodeMemoryUsage,
|
||||
width: { min: 240, default: 240 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={memory}>
|
||||
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Memory Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Memory Allocatable</div>,
|
||||
dataIndex: 'memory_allocatable',
|
||||
key: 'memory_allocatable',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'memory_allocatable',
|
||||
header: 'Memory Allocatable',
|
||||
accessorFn: (row): number => row.nodeMemoryAllocatable,
|
||||
width: { min: 240, default: 240 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryAllocatable = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={memoryAllocatable}>
|
||||
<TanStackTable.Text>{formatBytes(memoryAllocatable)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sNodesRenderRowData = (
|
||||
node: K8sNodeData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(
|
||||
node,
|
||||
() => node.nodeUID || node.meta.k8s_node_uid || node.meta.k8s_node_name,
|
||||
groupBy,
|
||||
),
|
||||
itemKey: node.meta.k8s_node_name,
|
||||
nodeUID: node.nodeUID || node.meta.k8s_node_uid,
|
||||
nodeName: (
|
||||
<Tooltip title={node.meta.k8s_node_name}>
|
||||
{node.meta.k8s_node_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
clusterName: node.meta.k8s_cluster_name,
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={node.nodeCPUUsage}>
|
||||
{node.nodeCPUUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={node.nodeMemoryUsage}>
|
||||
{formatBytes(node.nodeMemoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_allocatable: (
|
||||
<ValidateColumnValueWrapper value={node.nodeCPUAllocatable}>
|
||||
{node.nodeCPUAllocatable}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_allocatable: (
|
||||
<ValidateColumnValueWrapper value={node.nodeMemoryAllocatable}>
|
||||
{formatBytes(node.nodeMemoryAllocatable)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
nodeGroup: getGroupByEl(node, groupBy),
|
||||
...node.meta,
|
||||
groupedByMeta: getGroupedByMeta(node, groupBy),
|
||||
});
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
.entityGroupHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
}
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
podWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
k8sPodColumns,
|
||||
getK8sPodItemKey,
|
||||
getK8sPodRowKey,
|
||||
k8sPodColumnsConfig,
|
||||
k8sPodRenderRowData,
|
||||
} from './table.config';
|
||||
|
||||
function K8sPodsList({
|
||||
@@ -91,10 +91,10 @@ function K8sPodsList({
|
||||
<K8sBaseList<K8sPodsData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
tableColumnsDefinitions={k8sPodColumns}
|
||||
tableColumns={k8sPodColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sPodRenderRowData}
|
||||
getRowKey={getK8sPodRowKey}
|
||||
getItemKey={getK8sPodItemKey}
|
||||
eventCategory={InfraMonitoringEvents.Pod}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,328 +1,207 @@
|
||||
import React from 'react';
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Tooltip } from 'antd';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8s/components';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/types';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
formatBytes,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../commonUtils';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { K8sPodsData } from './api';
|
||||
import { Container } from 'lucide-react';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export interface K8sPodsRowData {
|
||||
key: string;
|
||||
podName: React.ReactNode;
|
||||
podUID: string;
|
||||
cpu_request: React.ReactNode;
|
||||
cpu_limit: React.ReactNode;
|
||||
cpu: React.ReactNode;
|
||||
memory_request: React.ReactNode;
|
||||
memory_limit: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
restarts: React.ReactNode;
|
||||
groupedByMeta?: any;
|
||||
export function getK8sPodRowKey(pod: K8sPodsData): string {
|
||||
return pod.podUID || pod.meta.k8s_pod_uid || pod.meta.k8s_pod_name;
|
||||
}
|
||||
|
||||
export const k8sPodColumns: IEntityColumn[] = [
|
||||
export function getK8sPodItemKey(pod: K8sPodsData): string {
|
||||
return pod.podUID;
|
||||
}
|
||||
|
||||
export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
|
||||
{
|
||||
label: 'Pod Group',
|
||||
value: 'podGroup',
|
||||
id: 'podGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="POD GROUP" />,
|
||||
accessorFn: (row): string => row.meta.k8s_pod_name || '',
|
||||
width: { min: 300 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-collapse',
|
||||
cell: ({ isExpanded, toggleExpanded, row }): JSX.Element | null => {
|
||||
return (
|
||||
<ExpandButtonWrapper
|
||||
isExpanded={isExpanded}
|
||||
toggleExpanded={toggleExpanded}
|
||||
>
|
||||
<K8sGroupCell row={row} />
|
||||
</ExpandButtonWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Pod name',
|
||||
value: 'podName',
|
||||
id: 'podName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'CPU Req Usage (%)',
|
||||
value: 'cpu_request',
|
||||
id: 'cpu_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Limit Usage (%)',
|
||||
value: 'cpu_limit',
|
||||
id: 'cpu_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Req Usage (%)',
|
||||
value: 'memory_request',
|
||||
id: 'memory_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Limit Usage (%)',
|
||||
value: 'memory_limit',
|
||||
id: 'memory_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Namespace name',
|
||||
value: 'namespace',
|
||||
id: 'namespace',
|
||||
canBeHidden: true,
|
||||
defaultVisibility: false,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Node name',
|
||||
value: 'node',
|
||||
id: 'node',
|
||||
canBeHidden: true,
|
||||
defaultVisibility: false,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Cluster name',
|
||||
value: 'cluster',
|
||||
id: 'cluster',
|
||||
canBeHidden: true,
|
||||
defaultVisibility: false,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
// TODO - Re-enable the column once backend issue is fixed
|
||||
// {
|
||||
// label: 'Restarts',
|
||||
// value: 'restarts',
|
||||
// id: 'restarts',
|
||||
// canRemove: false,
|
||||
// },
|
||||
];
|
||||
|
||||
export const k8sPodColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> POD GROUP
|
||||
</div>
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Pod Name"
|
||||
icon={<Container data-hide-expanded="true" size={14} />}
|
||||
/>
|
||||
),
|
||||
dataIndex: 'podGroup',
|
||||
key: 'podGroup',
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
sorter: false,
|
||||
accessorFn: (row): string => row.meta.k8s_pod_name || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const podName = value as string;
|
||||
return (
|
||||
<Tooltip title={podName}>
|
||||
<TanStackTable.Text>{podName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Pod Name</div>,
|
||||
dataIndex: 'podName',
|
||||
key: 'podName',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
sorter: false,
|
||||
id: 'cpu_request',
|
||||
header: 'CPU Req Usage (%)',
|
||||
accessorFn: (row): number => row.podCPURequest,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuRequest}
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>CPU Req Usage (%)</div>,
|
||||
dataIndex: 'cpu_request',
|
||||
key: 'cpu_request',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'cpu_limit',
|
||||
header: 'CPU Limit Usage (%)',
|
||||
accessorFn: (row): number => row.podCPULimit,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuLimit = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuLimit}
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>CPU Limit Usage (%)</div>,
|
||||
dataIndex: 'cpu_limit',
|
||||
key: 'cpu_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
accessorFn: (row): number => row.podCPU,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={cpu}>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'memory_request',
|
||||
header: 'Mem Req Usage (%)',
|
||||
accessorFn: (row): number => row.podMemoryRequest,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memoryRequest}
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Mem Req Usage (%)</div>,
|
||||
dataIndex: 'memory_request',
|
||||
key: 'memory_request',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'memory_limit',
|
||||
header: 'Mem Limit Usage (%)',
|
||||
accessorFn: (row): number => row.podMemoryLimit,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryLimit = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memoryLimit}
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Mem Limit Usage (%)</div>,
|
||||
dataIndex: 'memory_limit',
|
||||
key: 'memory_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'memory',
|
||||
header: 'Mem Usage (WSS)',
|
||||
accessorFn: (row): number => row.podMemory,
|
||||
width: { min: 210, default: '100%' },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={memory}>
|
||||
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'namespace',
|
||||
header: 'Namespace',
|
||||
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
|
||||
width: { default: 100 },
|
||||
enableSort: false,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
<TanStackTable.Text>{value as string}</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <div>Namespace</div>,
|
||||
dataIndex: 'namespace',
|
||||
key: 'namespace',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
ellipsis: true,
|
||||
align: 'left',
|
||||
id: 'node',
|
||||
header: 'Node',
|
||||
accessorFn: (row): string => row.meta.k8s_node_name || '',
|
||||
width: { default: 100 },
|
||||
enableSort: false,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
<TanStackTable.Text>{value as string}</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <div>Node</div>,
|
||||
dataIndex: 'node',
|
||||
key: 'node',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
ellipsis: true,
|
||||
align: 'left',
|
||||
id: 'cluster',
|
||||
header: 'Cluster',
|
||||
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
|
||||
width: { default: 100 },
|
||||
enableSort: false,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
<TanStackTable.Text>{value as string}</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <div>Cluster</div>,
|
||||
dataIndex: 'cluster',
|
||||
key: 'cluster',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
ellipsis: true,
|
||||
align: 'left',
|
||||
},
|
||||
// TODO - Re-enable the column once backend issue is fixed
|
||||
// {
|
||||
// title: (
|
||||
// <div className="column-header">
|
||||
// <Tooltip title="Container Restarts">Restarts</Tooltip>
|
||||
// </div>
|
||||
// ),
|
||||
// dataIndex: 'restarts',
|
||||
// key: 'restarts',
|
||||
// width: 40,
|
||||
// ellipsis: true,
|
||||
// sorter: true,
|
||||
// align: 'left',
|
||||
// className: `column ${columnProgressBarClassName}`,
|
||||
// },
|
||||
];
|
||||
|
||||
export const k8sPodRenderRowData = (
|
||||
pod: K8sPodsData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(
|
||||
pod,
|
||||
() => pod.podUID || pod.meta.k8s_pod_uid || pod.meta.k8s_pod_name,
|
||||
groupBy,
|
||||
),
|
||||
itemKey: pod.podUID,
|
||||
podName: (
|
||||
<Tooltip title={pod.meta.k8s_pod_name || ''}>
|
||||
{pod.meta.k8s_pod_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
podUID: pod.podUID || '',
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={pod.podCPURequest}
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={pod.podCPURequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={pod.podCPULimit}
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={pod.podCPULimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={pod.podCPU}>
|
||||
{pod.podCPU}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={pod.podMemoryRequest}
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={pod.podMemoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={pod.podMemoryLimit}
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={pod.podMemoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={pod.podMemory}>
|
||||
{formatBytes(pod.podMemory)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
restarts: (
|
||||
<ValidateColumnValueWrapper value={pod.restartCount}>
|
||||
{pod.restartCount}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
namespace: pod.meta.k8s_namespace_name,
|
||||
node: pod.meta.k8s_node_name,
|
||||
cluster: pod.meta.k8s_cluster_name,
|
||||
meta: pod.meta,
|
||||
podGroup: getGroupByEl(pod, groupBy),
|
||||
...pod.meta,
|
||||
groupedByMeta: getGroupedByMeta(pod, groupBy),
|
||||
});
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.entityGroupHeader {
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
statefulSetWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
k8sStatefulSetsColumns,
|
||||
getK8sStatefulSetItemKey,
|
||||
getK8sStatefulSetRowKey,
|
||||
k8sStatefulSetsColumnsConfig,
|
||||
k8sStatefulSetsRenderRowData,
|
||||
} from './table.config';
|
||||
|
||||
function K8sStatefulSetsList({
|
||||
@@ -91,10 +91,10 @@ function K8sStatefulSetsList({
|
||||
<K8sBaseList<K8sStatefulSetsData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
tableColumnsDefinitions={k8sStatefulSetsColumns}
|
||||
tableColumns={k8sStatefulSetsColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sStatefulSetsRenderRowData}
|
||||
getRowKey={getK8sStatefulSetRowKey}
|
||||
getItemKey={getK8sStatefulSetItemKey}
|
||||
eventCategory={InfraMonitoringEvents.StatefulSet}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,295 +1,239 @@
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Tooltip } from 'antd';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8s/components';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/types';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
formatBytes,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../commonUtils';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { K8sStatefulSetsData } from './api';
|
||||
import { ArrowUpDown } from 'lucide-react';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export const k8sStatefulSetsColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'StatefulSet Group',
|
||||
value: 'statefulSetGroup',
|
||||
id: 'statefulSetGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'StatefulSet Name',
|
||||
value: 'statefulsetName',
|
||||
id: 'statefulsetName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Available',
|
||||
value: 'available_pods',
|
||||
id: 'available_pods',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Desired',
|
||||
value: 'desired_pods',
|
||||
id: 'desired_pods',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Req Usage (%)',
|
||||
value: 'cpu_request',
|
||||
id: 'cpu_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Limit Usage (%)',
|
||||
value: 'cpu_limit',
|
||||
id: 'cpu_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Req Usage (%)',
|
||||
value: 'memory_request',
|
||||
id: 'memory_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Limit Usage (%)',
|
||||
value: 'memory_limit',
|
||||
id: 'memory_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sStatefulSetsColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> STATEFULSET GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'statefulSetGroup',
|
||||
key: 'statefulSetGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
},
|
||||
{
|
||||
title: <div>StatefulSet Name</div>,
|
||||
dataIndex: 'statefulsetName',
|
||||
key: 'statefulsetName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Namespace Name</div>,
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Available</div>,
|
||||
dataIndex: 'available_pods',
|
||||
key: 'available_pods',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Desired</div>,
|
||||
dataIndex: 'desired_pods',
|
||||
key: 'desired_pods',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Req Usage (%)</div>,
|
||||
dataIndex: 'cpu_request',
|
||||
key: 'cpu_request',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Limit Usage (%)</div>,
|
||||
dataIndex: 'cpu_limit',
|
||||
key: 'cpu_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Req Usage (%)</div>,
|
||||
dataIndex: 'memory_request',
|
||||
key: 'memory_request',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Limit Usage (%)</div>,
|
||||
dataIndex: 'memory_limit',
|
||||
key: 'memory_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sStatefulSetsRenderRowData = (
|
||||
export function getK8sStatefulSetRowKey(
|
||||
statefulSet: K8sStatefulSetsData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(
|
||||
statefulSet,
|
||||
() =>
|
||||
statefulSet.statefulSetName || statefulSet.meta.k8s_statefulset_name || '',
|
||||
groupBy,
|
||||
),
|
||||
itemKey: statefulSet.meta.k8s_statefulset_name,
|
||||
statefulsetName: (
|
||||
<Tooltip title={statefulSet.meta.k8s_statefulset_name}>
|
||||
{statefulSet.meta.k8s_statefulset_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
namespaceName: (
|
||||
<Tooltip title={statefulSet.meta.k8s_namespace_name}>
|
||||
{statefulSet.meta.k8s_namespace_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={statefulSet.cpuRequest}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={statefulSet.cpuRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={statefulSet.cpuLimit}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={statefulSet.cpuLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={statefulSet.cpuUsage}>
|
||||
{statefulSet.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={statefulSet.memoryRequest}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={statefulSet.memoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={statefulSet.memoryLimit}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={statefulSet.memoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={statefulSet.memoryUsage}>
|
||||
{formatBytes(statefulSet.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
available_pods: (
|
||||
<ValidateColumnValueWrapper value={statefulSet.availablePods}>
|
||||
{statefulSet.availablePods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
desired_pods: (
|
||||
<ValidateColumnValueWrapper value={statefulSet.desiredPods}>
|
||||
{statefulSet.desiredPods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
statefulSetGroup: getGroupByEl(statefulSet, groupBy),
|
||||
...statefulSet.meta,
|
||||
groupedByMeta: getGroupedByMeta(statefulSet, groupBy),
|
||||
});
|
||||
): string {
|
||||
return (
|
||||
statefulSet.statefulSetName || statefulSet.meta.k8s_statefulset_name || ''
|
||||
);
|
||||
}
|
||||
|
||||
export function getK8sStatefulSetItemKey(
|
||||
statefulSet: K8sStatefulSetsData,
|
||||
): string {
|
||||
return statefulSet.meta.k8s_statefulset_name;
|
||||
}
|
||||
|
||||
export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[] =
|
||||
[
|
||||
{
|
||||
id: 'statefulSetGroup',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader title="STATEFULSET GROUP" />
|
||||
),
|
||||
accessorFn: (row): string => row.meta.k8s_statefulset_name || '',
|
||||
width: { min: 210 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-collapse',
|
||||
cell: ({ isExpanded, toggleExpanded, row }): JSX.Element | null => {
|
||||
return (
|
||||
<ExpandButtonWrapper
|
||||
isExpanded={isExpanded}
|
||||
toggleExpanded={toggleExpanded}
|
||||
>
|
||||
<K8sGroupCell row={row} />
|
||||
</ExpandButtonWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'statefulsetName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="StatefulSet Name"
|
||||
icon={<ArrowUpDown data-hide-expanded="true" size={14} />}
|
||||
/>
|
||||
),
|
||||
accessorFn: (row): string => row.meta.k8s_statefulset_name || '',
|
||||
width: { min: 200 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const statefulsetName = value as string;
|
||||
return (
|
||||
<Tooltip title={statefulsetName}>
|
||||
<TanStackTable.Text>{statefulsetName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'namespaceName',
|
||||
header: 'Namespace Name',
|
||||
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
|
||||
width: { default: 150 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const namespaceName = value as string;
|
||||
return (
|
||||
<Tooltip title={namespaceName}>
|
||||
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'available_pods',
|
||||
header: 'Available',
|
||||
accessorFn: (row): number => row.availablePods,
|
||||
width: { min: 100, default: 140 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const availablePods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={availablePods}>
|
||||
<TanStackTable.Text>{availablePods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'desired_pods',
|
||||
header: 'Desired',
|
||||
accessorFn: (row): number => row.desiredPods,
|
||||
width: { min: 100, default: 140 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const desiredPods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={desiredPods}>
|
||||
<TanStackTable.Text>{desiredPods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: 'CPU Req Usage (%)',
|
||||
accessorFn: (row): number => row.cpuRequest,
|
||||
width: { min: 200, default: 200 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuRequest}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: 'CPU Limit Usage (%)',
|
||||
accessorFn: (row): number => row.cpuLimit,
|
||||
width: { min: 200, default: 200 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuLimit = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuLimit}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
accessorFn: (row): number => row.cpuUsage,
|
||||
width: { min: 190 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={cpu}>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: 'Mem Req Usage (%)',
|
||||
accessorFn: (row): number => row.memoryRequest,
|
||||
width: { min: 190 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memoryRequest}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: 'Mem Limit Usage (%)',
|
||||
accessorFn: (row): number => row.memoryLimit,
|
||||
width: { min: 180 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryLimit = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memoryLimit}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
header: 'Mem Usage (WSS)',
|
||||
accessorFn: (row): number => row.memoryUsage,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={memory}>
|
||||
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.entityGroupHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
volumeWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
k8sVolumesColumns,
|
||||
getK8sVolumeItemKey,
|
||||
getK8sVolumeRowKey,
|
||||
k8sVolumesColumnsConfig,
|
||||
k8sVolumesRenderRowData,
|
||||
} from './table.config';
|
||||
|
||||
function K8sVolumesList({
|
||||
@@ -91,10 +91,10 @@ function K8sVolumesList({
|
||||
<K8sBaseList<K8sVolumesData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.VOLUMES}
|
||||
tableColumnsDefinitions={k8sVolumesColumns}
|
||||
tableColumns={k8sVolumesColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sVolumesRenderRowData}
|
||||
getRowKey={getK8sVolumeRowKey}
|
||||
getItemKey={getK8sVolumeItemKey}
|
||||
eventCategory={InfraMonitoringEvents.Volumes}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,164 +1,131 @@
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Tooltip } from 'antd';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8s/components';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/types';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import { formatBytes, ValidateColumnValueWrapper } from '../commonUtils';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { ValidateColumnValueWrapper } from '../components';
|
||||
import { K8sVolumesData } from './api';
|
||||
import { HardDrive } from 'lucide-react';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
export function getK8sVolumeRowKey(volume: K8sVolumesData): string {
|
||||
return (
|
||||
volume.persistentVolumeClaimName ||
|
||||
volume.meta.k8s_persistentvolumeclaim_name ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
export const k8sVolumesColumns: IEntityColumn[] = [
|
||||
export function getK8sVolumeItemKey(volume: K8sVolumesData): string {
|
||||
return volume.persistentVolumeClaimName;
|
||||
}
|
||||
|
||||
export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
|
||||
{
|
||||
label: 'Volume Group',
|
||||
value: 'volumeGroup',
|
||||
id: 'volumeGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="VOLUME GROUP" />,
|
||||
accessorFn: (row): string => row.persistentVolumeClaimName || '',
|
||||
width: { min: 300 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-collapse',
|
||||
cell: ({ isExpanded, toggleExpanded, row }): JSX.Element | null => {
|
||||
return (
|
||||
<ExpandButtonWrapper
|
||||
isExpanded={isExpanded}
|
||||
toggleExpanded={toggleExpanded}
|
||||
>
|
||||
<K8sGroupCell row={row} />
|
||||
</ExpandButtonWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'PVC Name',
|
||||
value: 'pvcName',
|
||||
id: 'pvcName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Volume Capacity',
|
||||
value: 'capacity',
|
||||
id: 'capacity',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Volume Utilization',
|
||||
value: 'usage',
|
||||
id: 'usage',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Volume Available',
|
||||
value: 'available',
|
||||
id: 'available',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sVolumesColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> VOLUME GROUP
|
||||
</div>
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="PVC Name"
|
||||
icon={<HardDrive data-hide-expanded="true" size={14} />}
|
||||
/>
|
||||
),
|
||||
dataIndex: 'volumeGroup',
|
||||
key: 'volumeGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
accessorFn: (row): string => row.persistentVolumeClaimName || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const pvcName = value as string;
|
||||
return (
|
||||
<Tooltip title={pvcName}>
|
||||
<TanStackTable.Text>{pvcName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>PVC Name</div>,
|
||||
dataIndex: 'pvcName',
|
||||
key: 'pvcName',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
id: 'namespaceName',
|
||||
header: 'Namespace Name',
|
||||
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
|
||||
width: { min: 220 },
|
||||
enableSort: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const namespaceName = value as string;
|
||||
return (
|
||||
<Tooltip title={namespaceName}>
|
||||
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Namespace Name</div>,
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
id: 'capacity',
|
||||
header: 'Volume Capacity',
|
||||
accessorFn: (row): number => row.volumeCapacity,
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const capacity = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={capacity}>
|
||||
<TanStackTable.Text>{formatBytes(capacity)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Volume Capacity</div>,
|
||||
dataIndex: 'capacity',
|
||||
key: 'capacity',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'usage',
|
||||
header: 'Volume Utilization',
|
||||
accessorFn: (row): number => row.volumeUsage,
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const usage = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={usage}>
|
||||
<TanStackTable.Text>{formatBytes(usage)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div>Volume Utilization</div>,
|
||||
dataIndex: 'usage',
|
||||
key: 'usage',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Volume Available</div>,
|
||||
dataIndex: 'available',
|
||||
key: 'available',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
id: 'available',
|
||||
header: 'Volume Available',
|
||||
accessorFn: (row): number => row.volumeAvailable,
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const available = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper value={available}>
|
||||
<TanStackTable.Text>{formatBytes(available)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sVolumesRenderRowData = (
|
||||
volume: K8sVolumesData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(
|
||||
volume,
|
||||
() =>
|
||||
volume.persistentVolumeClaimName ||
|
||||
volume.meta.k8s_persistentvolumeclaim_name ||
|
||||
'',
|
||||
groupBy,
|
||||
),
|
||||
itemKey: volume.persistentVolumeClaimName,
|
||||
pvcName: (
|
||||
<Tooltip title={volume.persistentVolumeClaimName}>
|
||||
{volume.persistentVolumeClaimName || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
namespaceName: (
|
||||
<Tooltip title={volume.meta.k8s_namespace_name}>
|
||||
{volume.meta.k8s_namespace_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
available: (
|
||||
<ValidateColumnValueWrapper value={volume.volumeAvailable}>
|
||||
{formatBytes(volume.volumeAvailable)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
capacity: (
|
||||
<ValidateColumnValueWrapper value={volume.volumeCapacity}>
|
||||
{formatBytes(volume.volumeCapacity)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
usage: (
|
||||
<ValidateColumnValueWrapper value={volume.volumeUsage}>
|
||||
{formatBytes(volume.volumeUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
volumeGroup: getGroupByEl(volume, groupBy),
|
||||
...volume.meta,
|
||||
groupedByMeta: getGroupedByMeta(volume, groupBy),
|
||||
});
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
.entityGroupHeader {
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -1,21 +1,11 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { EntityProgressBar, EventContents } from '../commonUtils';
|
||||
|
||||
jest.mock('../commonUtils.module.scss', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
entityProgressBar: 'entity-progress-bar-module',
|
||||
progressBar: 'progress-bar-module',
|
||||
eventContentContainer: 'event-content-container-module',
|
||||
},
|
||||
}));
|
||||
import { EntityProgressBar } from '../components';
|
||||
import { EventContents } from '../commonUtils';
|
||||
|
||||
jest.mock('components/ResizeTable', () => ({
|
||||
ResizeTable: ({ className, dataSource }: any): JSX.Element => (
|
||||
<div data-testid="resize-table" className={className}>
|
||||
{JSON.stringify(dataSource)}
|
||||
</div>
|
||||
ResizeTable: ({ dataSource }: { dataSource: unknown }): JSX.Element => (
|
||||
<div data-testid="resize-table">{JSON.stringify(dataSource)}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -25,24 +15,22 @@ jest.mock('container/LogDetailedView/FieldRenderer', () => ({
|
||||
}));
|
||||
|
||||
describe('commonUtils', () => {
|
||||
it('renders EntityProgressBar with module classes', () => {
|
||||
const { container } = render(
|
||||
<EntityProgressBar value={0.5} type="request" />,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toHaveClass('entity-progress-bar-module');
|
||||
expect(container.querySelector('.progress-bar-module')).toBeInTheDocument();
|
||||
it('renders EntityProgressBar with percentage value', () => {
|
||||
render(<EntityProgressBar value={0.5} type="request" />);
|
||||
expect(screen.getByText('50%')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders EventContents with the module-scoped table class', () => {
|
||||
it('renders EntityProgressBar with dash for NaN value', () => {
|
||||
render(<EntityProgressBar value={NaN} type="limit" />);
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders EventContents with data fields', () => {
|
||||
render(
|
||||
<EventContents data={{ namespace: 'default', cluster: 'prod-cluster' }} />,
|
||||
);
|
||||
|
||||
const resizeTable = screen.getByTestId('resize-table');
|
||||
|
||||
expect(resizeTable).toHaveClass('event-content-container-module');
|
||||
expect(resizeTable).toHaveTextContent('namespace');
|
||||
expect(resizeTable).toHaveTextContent('prod-cluster');
|
||||
});
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
.entityProgressBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 0;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.eventContentContainer {
|
||||
:global(.ant-table) {
|
||||
background: var(--l1-background);
|
||||
|
||||
@@ -2,17 +2,12 @@
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, Table, Tooltip, Typography } from 'antd';
|
||||
import { Table, Tooltip } from 'antd';
|
||||
import type { ColumnsType } from 'antd/lib/table';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import FieldRenderer from 'container/LogDetailedView/FieldRenderer';
|
||||
import { DataType } from 'container/LogDetailedView/TableView';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { getInvalidValueTooltipText, InfraMonitoringEntity } from './constants';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import styles from './commonUtils.module.scss';
|
||||
|
||||
@@ -20,6 +15,10 @@ import styles from './commonUtils.module.scss';
|
||||
* Converts size in bytes to a human-readable string with appropriate units
|
||||
*/
|
||||
export function formatBytes(bytes: number, decimals = 2): string {
|
||||
if (Number.isNaN(bytes) || !Number.isFinite(bytes)) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
if (bytes === 0) {
|
||||
return '0 Bytes';
|
||||
}
|
||||
@@ -31,36 +30,6 @@ export function formatBytes(bytes: number, decimals = 2): string {
|
||||
return `${parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper component that renders its children for valid values or renders '-' for invalid values (-1)
|
||||
*/
|
||||
export function ValidateColumnValueWrapper({
|
||||
children,
|
||||
value,
|
||||
entity,
|
||||
attribute,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
value: number;
|
||||
entity?: InfraMonitoringEntity;
|
||||
attribute?: string;
|
||||
}): JSX.Element {
|
||||
if (value === -1) {
|
||||
let element = <div>-</div>;
|
||||
if (entity && attribute) {
|
||||
element = (
|
||||
<Tooltip title={getInvalidValueTooltipText(entity, attribute)}>
|
||||
{element}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns stroke color for request utilization parameters according to current value
|
||||
*/
|
||||
@@ -103,35 +72,6 @@ export function getStrokeColorForLimitUtilization(value: number): string {
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
|
||||
export function EntityProgressBar({
|
||||
value,
|
||||
type,
|
||||
}: {
|
||||
value: number;
|
||||
type: 'request' | 'limit';
|
||||
}): JSX.Element {
|
||||
const percentage = Number((value * 100).toFixed(1));
|
||||
|
||||
return (
|
||||
<div className={styles.entityProgressBar}>
|
||||
<Progress
|
||||
percent={percentage}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
status="normal"
|
||||
strokeColor={
|
||||
type === 'limit'
|
||||
? getStrokeColorForLimitUtilization(value)
|
||||
: getStrokeColorForRequestUtilization(value)
|
||||
}
|
||||
className={styles.progressBar}
|
||||
showInfo={false}
|
||||
/>
|
||||
<Typography.Text style={{ fontSize: '10px' }}>{percentage}%</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EventContents({
|
||||
data,
|
||||
}: {
|
||||
@@ -248,19 +188,3 @@ export const filterDuplicateFilters = (
|
||||
|
||||
return uniqueFilters;
|
||||
};
|
||||
|
||||
export const getFiltersFromParams = (
|
||||
searchParams: URLSearchParams,
|
||||
queryKey: string,
|
||||
): IBuilderQuery['filters'] | null => {
|
||||
const filtersFromParams = searchParams.get(queryKey);
|
||||
if (filtersFromParams) {
|
||||
try {
|
||||
const parsed = JSON.parse(filtersFromParams);
|
||||
return parsed as IBuilderQuery['filters'];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
.entityProgressBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 0;
|
||||
min-width: 100px;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Progress } from 'antd';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import {
|
||||
getMemoryProgressColor,
|
||||
getProgressColor,
|
||||
} from 'container/InfraMonitoringHosts/constants';
|
||||
|
||||
import {
|
||||
getStrokeColorForLimitUtilization,
|
||||
getStrokeColorForRequestUtilization,
|
||||
} from '../commonUtils';
|
||||
|
||||
import styles from './EntityProgressBar.module.scss';
|
||||
|
||||
type EntityProgressBarType = 'request' | 'limit' | 'cpu' | 'memory';
|
||||
|
||||
function getStrokeColor(type: EntityProgressBarType, value: number): string {
|
||||
switch (type) {
|
||||
case 'limit':
|
||||
return getStrokeColorForLimitUtilization(value);
|
||||
case 'request':
|
||||
return getStrokeColorForRequestUtilization(value);
|
||||
case 'cpu':
|
||||
return getProgressColor(Number((value * 100).toFixed(1)));
|
||||
case 'memory':
|
||||
return getMemoryProgressColor(Number((value * 100).toFixed(1)));
|
||||
default:
|
||||
return getStrokeColorForRequestUtilization(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function EntityProgressBar({
|
||||
value,
|
||||
type,
|
||||
}: {
|
||||
value: number;
|
||||
type: EntityProgressBarType;
|
||||
}): JSX.Element {
|
||||
const percentage = Number.isNaN(+value)
|
||||
? null
|
||||
: Number((value * 100).toFixed(1));
|
||||
|
||||
if (percentage === null) {
|
||||
return (
|
||||
<div className={styles.entityProgressBar}>
|
||||
<TanStackTable.Text>-</TanStackTable.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.entityProgressBar}>
|
||||
<Progress
|
||||
percent={percentage}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
status="normal"
|
||||
strokeColor={getStrokeColor(type, value)}
|
||||
className={styles.progressBar}
|
||||
showInfo={false}
|
||||
/>
|
||||
<TanStackTable.Text>{percentage}%</TanStackTable.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Button } from '@signozhq/ui';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import styles from './ExpandedButtonWrapper.module.scss';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function ExpandButtonWrapper({
|
||||
toggleExpanded,
|
||||
isExpanded,
|
||||
children,
|
||||
}: {
|
||||
toggleExpanded: () => void;
|
||||
isExpanded: boolean;
|
||||
children?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
// the state is duplicated because it takes a few ms to propagate using isExpanded
|
||||
// so this local is used to avoid this delay
|
||||
const [localIsExpanded, setLocalIsExpanded] = useState(isExpanded);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalIsExpanded(isExpanded);
|
||||
}, [isExpanded]);
|
||||
|
||||
return (
|
||||
<div className={styles.expandButtonContainer}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
setLocalIsExpanded((v) => !v);
|
||||
toggleExpanded();
|
||||
}}
|
||||
size="icon"
|
||||
prefix={localIsExpanded ? <ChevronDown /> : <ChevronRight />}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.expandButtonContainer {
|
||||
display: flex;
|
||||
|
||||
& > button {
|
||||
margin-right: var(--spacing-4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
|
||||
import {
|
||||
getInvalidValueTooltipText,
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
|
||||
export function ValidateColumnValueWrapper({
|
||||
children,
|
||||
value,
|
||||
entity,
|
||||
attribute,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
value: number;
|
||||
entity?: InfraMonitoringEntity;
|
||||
attribute?: string;
|
||||
}): JSX.Element {
|
||||
if (value === -1) {
|
||||
let element = <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
if (entity && attribute) {
|
||||
element = (
|
||||
<Tooltip title={getInvalidValueTooltipText(entity, attribute)}>
|
||||
{element}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { EntityProgressBar } from './EntityProgressBar';
|
||||
export { ValidateColumnValueWrapper } from './ValidateColumnValueWrapper';
|
||||
export { ExpandButtonWrapper } from './ExpandButtonWrapper';
|
||||
@@ -800,5 +800,8 @@ export const INFRA_MONITORING_K8S_PARAMS_KEYS = {
|
||||
EVENTS_FILTERS: 'eventsFilters',
|
||||
HOSTS_FILTERS: 'hostsFilters',
|
||||
CURRENT_PAGE: 'currentPage',
|
||||
PAGE: 'page',
|
||||
PAGE_SIZE: 'pageSize',
|
||||
EXPANDED: 'expanded',
|
||||
SELECTED_ITEM: 'selectedItem',
|
||||
};
|
||||
|
||||
@@ -13,8 +13,11 @@ import {
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
|
||||
import { VIEWS } from './constants';
|
||||
import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategories } from './constants';
|
||||
import {
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
K8sCategories,
|
||||
VIEWS,
|
||||
} from './constants';
|
||||
import { orderBySchema, OrderBySchemaType } from './schemas';
|
||||
|
||||
const defaultNuqsOptions: Options = {
|
||||
@@ -30,6 +33,24 @@ export const useInfraMonitoringCurrentPage = (): UseQueryStateReturn<
|
||||
parseAsInteger.withDefault(1).withOptions(defaultNuqsOptions),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringPageListing = (): UseQueryStateReturn<
|
||||
number,
|
||||
number
|
||||
> =>
|
||||
useQueryState(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
|
||||
parseAsInteger.withDefault(1).withOptions(defaultNuqsOptions),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringPageSizeListing = (): UseQueryStateReturn<
|
||||
number,
|
||||
number
|
||||
> =>
|
||||
useQueryState(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,
|
||||
parseAsInteger.withDefault(10).withOptions(defaultNuqsOptions),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringOrderBy = (): UseQueryStateReturn<
|
||||
OrderBySchemaType,
|
||||
OrderBySchemaType
|
||||
@@ -98,7 +119,7 @@ export const useInfraMonitoringCategory = (): UseQueryStateReturn<
|
||||
parseAsString.withDefault(K8sCategories.PODS).withOptions(defaultNuqsOptions),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringFilters = (): UseQueryStateReturn<
|
||||
export const useInfraMonitoringFiltersK8s = (): UseQueryStateReturn<
|
||||
TagFilter,
|
||||
undefined
|
||||
> =>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { GoogleSquareFilled, KeyOutlined } from '@ant-design/icons';
|
||||
import { Button, Typography } from 'antd';
|
||||
import { AuthtypesAuthNProviderDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import './CreateEdit.styles.scss';
|
||||
|
||||
interface AuthNProvider {
|
||||
key: string;
|
||||
key: AuthtypesAuthNProviderDTO;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: JSX.Element;
|
||||
@@ -14,14 +15,14 @@ interface AuthNProvider {
|
||||
function getAuthNProviders(samlEnabled: boolean): AuthNProvider[] {
|
||||
return [
|
||||
{
|
||||
key: 'google_auth',
|
||||
key: AuthtypesAuthNProviderDTO.google_auth,
|
||||
title: 'Google Apps Authentication',
|
||||
description: 'Let members sign-in with a Google workspace account',
|
||||
icon: <GoogleSquareFilled style={{ fontSize: '37px' }} />,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
key: 'saml',
|
||||
key: AuthtypesAuthNProviderDTO.saml,
|
||||
title: 'SAML Authentication',
|
||||
description:
|
||||
'Azure, Active Directory, Okta or your custom SAML 2.0 solution',
|
||||
@@ -30,7 +31,7 @@ function getAuthNProviders(samlEnabled: boolean): AuthNProvider[] {
|
||||
},
|
||||
|
||||
{
|
||||
key: 'oidc',
|
||||
key: AuthtypesAuthNProviderDTO.oidc,
|
||||
title: 'OIDC Authentication',
|
||||
description:
|
||||
'Authenticate using OpenID Connect providers like Azure, Active Directory, Okta, or other OIDC compliant solutions',
|
||||
@@ -44,7 +45,9 @@ function AuthnProviderSelector({
|
||||
setAuthnProvider,
|
||||
samlEnabled,
|
||||
}: {
|
||||
setAuthnProvider: React.Dispatch<React.SetStateAction<string>>;
|
||||
setAuthnProvider: React.Dispatch<
|
||||
React.SetStateAction<AuthtypesAuthNProviderDTO | ''>
|
||||
>;
|
||||
samlEnabled: boolean;
|
||||
}): JSX.Element {
|
||||
const authnProviders = getAuthNProviders(samlEnabled);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useUpdateAuthDomain,
|
||||
} from 'api/generated/services/authdomains';
|
||||
import {
|
||||
AuthtypesAuthNProviderDTO,
|
||||
AuthtypesGettableAuthDomainDTO,
|
||||
AuthtypesGoogleConfigDTO,
|
||||
AuthtypesRoleMappingDTO,
|
||||
@@ -57,9 +58,9 @@ interface CreateOrEditProps {
|
||||
function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
const { isCreate, record, onClose } = props;
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const [authnProvider, setAuthnProvider] = useState<string>(
|
||||
record?.ssoType || '',
|
||||
);
|
||||
const [authnProvider, setAuthnProvider] = useState<
|
||||
AuthtypesAuthNProviderDTO | ''
|
||||
>(record?.config?.ssoType || '');
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const { featureFlags } = useAppContext();
|
||||
@@ -138,6 +139,10 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
return;
|
||||
}
|
||||
|
||||
if (authnProvider === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = form.getFieldValue('name');
|
||||
const googleAuthConfig = getGoogleAuthConfig();
|
||||
const samlConfig = form.getFieldValue('samlConfig');
|
||||
|
||||
@@ -112,21 +112,26 @@ export function prepareInitialValues(
|
||||
};
|
||||
}
|
||||
|
||||
const config = record.config ?? {};
|
||||
return {
|
||||
...record,
|
||||
googleAuthConfig: record.googleAuthConfig
|
||||
name: record.name,
|
||||
ssoEnabled: config.ssoEnabled,
|
||||
ssoType: config.ssoType,
|
||||
samlConfig: config.samlConfig ?? undefined,
|
||||
oidcConfig: config.oidcConfig ?? undefined,
|
||||
googleAuthConfig: config.googleAuthConfig
|
||||
? {
|
||||
...record.googleAuthConfig,
|
||||
...config.googleAuthConfig,
|
||||
domainToAdminEmailList: convertDomainMappingsToList(
|
||||
record.googleAuthConfig.domainToAdminEmail,
|
||||
config.googleAuthConfig.domainToAdminEmail,
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
roleMapping: record.roleMapping
|
||||
roleMapping: config.roleMapping
|
||||
? {
|
||||
...record.roleMapping,
|
||||
...config.roleMapping,
|
||||
groupMappingsList: convertGroupMappingsToList(
|
||||
record.roleMapping.groupMappings,
|
||||
config.roleMapping.groupMappings,
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
|
||||
@@ -43,11 +43,11 @@ function SSOEnforcementToggle({
|
||||
data: {
|
||||
config: {
|
||||
ssoEnabled: checked,
|
||||
ssoType: record.ssoType,
|
||||
googleAuthConfig: record.googleAuthConfig,
|
||||
oidcConfig: record.oidcConfig,
|
||||
samlConfig: record.samlConfig,
|
||||
roleMapping: record.roleMapping,
|
||||
ssoType: record.config?.ssoType,
|
||||
googleAuthConfig: record.config?.googleAuthConfig,
|
||||
oidcConfig: record.config?.oidcConfig,
|
||||
samlConfig: record.config?.samlConfig,
|
||||
roleMapping: record.config?.roleMapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -55,7 +55,10 @@ describe('SSOEnforcementToggle', () => {
|
||||
render(
|
||||
<SSOEnforcementToggle
|
||||
isDefaultChecked={false}
|
||||
record={{ ...mockGoogleAuthDomain, ssoEnabled: false }}
|
||||
record={{
|
||||
...mockGoogleAuthDomain,
|
||||
config: { ...mockGoogleAuthDomain.config, ssoEnabled: false },
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { AuthtypesGettableAuthDomainDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
AuthtypesAuthNProviderDTO,
|
||||
AuthtypesGettableAuthDomainDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
// API Endpoints
|
||||
export const AUTH_DOMAINS_LIST_ENDPOINT = '*/api/v1/domains';
|
||||
@@ -10,11 +13,13 @@ export const AUTH_DOMAINS_DELETE_ENDPOINT = '*/api/v1/domains/:id';
|
||||
export const mockGoogleAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-1',
|
||||
name: 'signoz.io',
|
||||
ssoEnabled: true,
|
||||
ssoType: 'google_auth',
|
||||
googleAuthConfig: {
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.google_auth,
|
||||
googleAuthConfig: {
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
},
|
||||
},
|
||||
authNProviderInfo: {
|
||||
relayStatePath: 'api/v1/sso/relay/domain-1',
|
||||
@@ -25,12 +30,14 @@ export const mockGoogleAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
export const mockSamlAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-2',
|
||||
name: 'example.com',
|
||||
ssoEnabled: false,
|
||||
ssoType: 'saml',
|
||||
samlConfig: {
|
||||
samlIdp: 'https://idp.example.com/sso',
|
||||
samlEntity: 'urn:example:idp',
|
||||
samlCert: 'MOCK_CERTIFICATE',
|
||||
config: {
|
||||
ssoEnabled: false,
|
||||
ssoType: AuthtypesAuthNProviderDTO.saml,
|
||||
samlConfig: {
|
||||
samlIdp: 'https://idp.example.com/sso',
|
||||
samlEntity: 'urn:example:idp',
|
||||
samlCert: 'MOCK_CERTIFICATE',
|
||||
},
|
||||
},
|
||||
authNProviderInfo: {
|
||||
relayStatePath: 'api/v1/sso/relay/domain-2',
|
||||
@@ -41,12 +48,14 @@ export const mockSamlAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
export const mockOidcAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-3',
|
||||
name: 'corp.io',
|
||||
ssoEnabled: true,
|
||||
ssoType: 'oidc',
|
||||
oidcConfig: {
|
||||
issuer: 'https://oidc.corp.io',
|
||||
clientId: 'oidc-client-id',
|
||||
clientSecret: 'oidc-client-secret',
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.oidc,
|
||||
oidcConfig: {
|
||||
issuer: 'https://oidc.corp.io',
|
||||
clientId: 'oidc-client-id',
|
||||
clientSecret: 'oidc-client-secret',
|
||||
},
|
||||
},
|
||||
authNProviderInfo: {
|
||||
relayStatePath: 'api/v1/sso/relay/domain-3',
|
||||
@@ -57,20 +66,22 @@ export const mockOidcAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
export const mockDomainWithRoleMapping: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-4',
|
||||
name: 'enterprise.com',
|
||||
ssoEnabled: true,
|
||||
ssoType: 'saml',
|
||||
samlConfig: {
|
||||
samlIdp: 'https://idp.enterprise.com/sso',
|
||||
samlEntity: 'urn:enterprise:idp',
|
||||
samlCert: 'MOCK_CERTIFICATE',
|
||||
},
|
||||
roleMapping: {
|
||||
defaultRole: 'EDITOR',
|
||||
useRoleAttribute: false,
|
||||
groupMappings: {
|
||||
'admin-group': 'ADMIN',
|
||||
'dev-team': 'EDITOR',
|
||||
viewers: 'VIEWER',
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.saml,
|
||||
samlConfig: {
|
||||
samlIdp: 'https://idp.enterprise.com/sso',
|
||||
samlEntity: 'urn:enterprise:idp',
|
||||
samlCert: 'MOCK_CERTIFICATE',
|
||||
},
|
||||
roleMapping: {
|
||||
defaultRole: 'EDITOR',
|
||||
useRoleAttribute: false,
|
||||
groupMappings: {
|
||||
'admin-group': 'ADMIN',
|
||||
'dev-team': 'EDITOR',
|
||||
viewers: 'VIEWER',
|
||||
},
|
||||
},
|
||||
},
|
||||
authNProviderInfo: {
|
||||
@@ -83,16 +94,18 @@ export const mockDomainWithDirectRoleAttribute: AuthtypesGettableAuthDomainDTO =
|
||||
{
|
||||
id: 'domain-5',
|
||||
name: 'direct-role.com',
|
||||
ssoEnabled: true,
|
||||
ssoType: 'oidc',
|
||||
oidcConfig: {
|
||||
issuer: 'https://oidc.direct-role.com',
|
||||
clientId: 'direct-role-client-id',
|
||||
clientSecret: 'direct-role-client-secret',
|
||||
},
|
||||
roleMapping: {
|
||||
defaultRole: 'VIEWER',
|
||||
useRoleAttribute: true,
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.oidc,
|
||||
oidcConfig: {
|
||||
issuer: 'https://oidc.direct-role.com',
|
||||
clientId: 'direct-role-client-id',
|
||||
clientSecret: 'direct-role-client-secret',
|
||||
},
|
||||
roleMapping: {
|
||||
defaultRole: 'VIEWER',
|
||||
useRoleAttribute: true,
|
||||
},
|
||||
},
|
||||
authNProviderInfo: {
|
||||
relayStatePath: 'api/v1/sso/relay/domain-5',
|
||||
@@ -103,20 +116,22 @@ export const mockDomainWithDirectRoleAttribute: AuthtypesGettableAuthDomainDTO =
|
||||
export const mockOidcWithClaimMapping: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-6',
|
||||
name: 'oidc-claims.com',
|
||||
ssoEnabled: true,
|
||||
ssoType: 'oidc',
|
||||
oidcConfig: {
|
||||
issuer: 'https://oidc.claims.com',
|
||||
issuerAlias: 'https://alias.claims.com',
|
||||
clientId: 'claims-client-id',
|
||||
clientSecret: 'claims-client-secret',
|
||||
insecureSkipEmailVerified: true,
|
||||
getUserInfo: true,
|
||||
claimMapping: {
|
||||
email: 'user_email',
|
||||
name: 'display_name',
|
||||
groups: 'user_groups',
|
||||
role: 'user_role',
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.oidc,
|
||||
oidcConfig: {
|
||||
issuer: 'https://oidc.claims.com',
|
||||
issuerAlias: 'https://alias.claims.com',
|
||||
clientId: 'claims-client-id',
|
||||
clientSecret: 'claims-client-secret',
|
||||
insecureSkipEmailVerified: true,
|
||||
getUserInfo: true,
|
||||
claimMapping: {
|
||||
email: 'user_email',
|
||||
name: 'display_name',
|
||||
groups: 'user_groups',
|
||||
role: 'user_role',
|
||||
},
|
||||
},
|
||||
},
|
||||
authNProviderInfo: {
|
||||
@@ -128,17 +143,19 @@ export const mockOidcWithClaimMapping: AuthtypesGettableAuthDomainDTO = {
|
||||
export const mockSamlWithAttributeMapping: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-7',
|
||||
name: 'saml-attrs.com',
|
||||
ssoEnabled: true,
|
||||
ssoType: 'saml',
|
||||
samlConfig: {
|
||||
samlIdp: 'https://idp.saml-attrs.com/sso',
|
||||
samlEntity: 'urn:saml-attrs:idp',
|
||||
samlCert: 'MOCK_CERTIFICATE_ATTRS',
|
||||
insecureSkipAuthNRequestsSigned: true,
|
||||
attributeMapping: {
|
||||
name: 'user_display_name',
|
||||
groups: 'member_of',
|
||||
role: 'signoz_role',
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.saml,
|
||||
samlConfig: {
|
||||
samlIdp: 'https://idp.saml-attrs.com/sso',
|
||||
samlEntity: 'urn:saml-attrs:idp',
|
||||
samlCert: 'MOCK_CERTIFICATE_ATTRS',
|
||||
insecureSkipAuthNRequestsSigned: true,
|
||||
attributeMapping: {
|
||||
name: 'user_display_name',
|
||||
groups: 'member_of',
|
||||
role: 'signoz_role',
|
||||
},
|
||||
},
|
||||
},
|
||||
authNProviderInfo: {
|
||||
@@ -151,19 +168,21 @@ export const mockGoogleAuthWithWorkspaceGroups: AuthtypesGettableAuthDomainDTO =
|
||||
{
|
||||
id: 'domain-8',
|
||||
name: 'google-groups.com',
|
||||
ssoEnabled: true,
|
||||
ssoType: 'google_auth',
|
||||
googleAuthConfig: {
|
||||
clientId: 'google-groups-client-id',
|
||||
clientSecret: 'google-groups-client-secret',
|
||||
insecureSkipEmailVerified: false,
|
||||
fetchGroups: true,
|
||||
serviceAccountJson: '{"type": "service_account"}',
|
||||
domainToAdminEmail: {
|
||||
'google-groups.com': 'admin@google-groups.com',
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.google_auth,
|
||||
googleAuthConfig: {
|
||||
clientId: 'google-groups-client-id',
|
||||
clientSecret: 'google-groups-client-secret',
|
||||
insecureSkipEmailVerified: false,
|
||||
fetchGroups: true,
|
||||
serviceAccountJson: '{"type": "service_account"}',
|
||||
domainToAdminEmail: {
|
||||
'google-groups.com': 'admin@google-groups.com',
|
||||
},
|
||||
fetchTransitiveGroupMembership: true,
|
||||
allowedGroups: ['allowed-group-1', 'allowed-group-2'],
|
||||
},
|
||||
fetchTransitiveGroupMembership: true,
|
||||
allowedGroups: ['allowed-group-1', 'allowed-group-2'],
|
||||
},
|
||||
authNProviderInfo: {
|
||||
relayStatePath: 'api/v1/sso/relay/domain-8',
|
||||
@@ -188,15 +207,19 @@ export const mockSingleDomainResponse = {
|
||||
data: [mockGoogleAuthDomain],
|
||||
};
|
||||
|
||||
// Mock success responses
|
||||
// Mock success responses. CreateAuthDomain returns just an Identifiable
|
||||
// (the new domain ID); clients re-Read to get the full domain.
|
||||
export const mockCreateSuccessResponse = {
|
||||
status: 'success',
|
||||
data: mockGoogleAuthDomain,
|
||||
data: { id: mockGoogleAuthDomain.id },
|
||||
};
|
||||
|
||||
export const mockUpdateSuccessResponse = {
|
||||
status: 'success',
|
||||
data: { ...mockGoogleAuthDomain, ssoEnabled: false },
|
||||
data: {
|
||||
...mockGoogleAuthDomain,
|
||||
config: { ...mockGoogleAuthDomain.config, ssoEnabled: false },
|
||||
},
|
||||
};
|
||||
|
||||
export const mockDeleteSuccessResponse = {
|
||||
|
||||
@@ -158,7 +158,7 @@ function AuthDomain(): JSX.Element {
|
||||
onClick={(): void => setRecord(record)}
|
||||
variant="link"
|
||||
>
|
||||
Configure {SSOType.get(record.ssoType || '')}
|
||||
Configure {SSOType.get(record.config?.ssoType || '')}
|
||||
</Button>
|
||||
<Button
|
||||
className="auth-domain-list-action-link delete"
|
||||
|
||||
@@ -17,7 +17,7 @@ import dayjs, { Dayjs } from 'dayjs';
|
||||
import {
|
||||
useGlobalTimeQueryInvalidate,
|
||||
useIsGlobalTimeQueryRefreshing,
|
||||
} from 'hooks/globalTime';
|
||||
} from 'store/globalTime';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
@@ -101,14 +101,14 @@ function DateTimeSelection({
|
||||
if (modalInitialStartTime !== undefined) {
|
||||
initialModalStartTime = modalInitialStartTime;
|
||||
} else if (searchStartTime) {
|
||||
initialModalStartTime = parseInt(searchStartTime, 10);
|
||||
initialModalStartTime = Number.parseInt(searchStartTime, 10);
|
||||
}
|
||||
|
||||
let initialModalEndTime = 0;
|
||||
if (modalInitialEndTime !== undefined) {
|
||||
initialModalEndTime = modalInitialEndTime;
|
||||
} else if (searchEndTime) {
|
||||
initialModalEndTime = parseInt(searchEndTime, 10);
|
||||
initialModalEndTime = Number.parseInt(searchEndTime, 10);
|
||||
}
|
||||
|
||||
const [modalStartTime, setModalStartTime] = useState<number>(
|
||||
@@ -159,9 +159,11 @@ function DateTimeSelection({
|
||||
const getTime = useCallback((): [number, number] | undefined => {
|
||||
if (searchEndTime && searchStartTime) {
|
||||
const startDate = dayjs(
|
||||
new Date(parseInt(getTimeString(searchStartTime), 10)),
|
||||
new Date(Number.parseInt(getTimeString(searchStartTime), 10)),
|
||||
);
|
||||
const endDate = dayjs(
|
||||
new Date(Number.parseInt(getTimeString(searchEndTime), 10)),
|
||||
);
|
||||
const endDate = dayjs(new Date(parseInt(getTimeString(searchEndTime), 10)));
|
||||
|
||||
return [startDate.toDate().getTime() || 0, endDate.toDate().getTime() || 0];
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export { useGlobalTimeQueryInvalidate } from './useGlobalTimeQueryInvalidate';
|
||||
export { useIsGlobalTimeQueryRefreshing } from './useIsGlobalTimeQueryRefreshing';
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
|
||||
/**
|
||||
* Use when you want to invalida any query tracked by {@link REACT_QUERY_KEY.AUTO_REFRESH_QUERY}
|
||||
*/
|
||||
export function useGlobalTimeQueryInvalidate(): () => Promise<void> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useCallback(async () => {
|
||||
return await queryClient.invalidateQueries({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY],
|
||||
});
|
||||
}, [queryClient]);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useIsFetching } from 'react-query';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
|
||||
/**
|
||||
* Use when you want to know if any query tracked by {@link REACT_QUERY_KEY.AUTO_REFRESH_QUERY} is refreshing
|
||||
*/
|
||||
export function useIsGlobalTimeQueryRefreshing(): boolean {
|
||||
return (
|
||||
useIsFetching({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY],
|
||||
}) > 0
|
||||
);
|
||||
}
|
||||
71
frontend/src/store/globalTime/GlobalTimeContext.tsx
Normal file
71
frontend/src/store/globalTime/GlobalTimeContext.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
// oxlint-disable-next-line no-restricted-imports
|
||||
createContext,
|
||||
ReactNode,
|
||||
// oxlint-disable-next-line no-restricted-imports
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
|
||||
import get from 'api/browser/localstorage/get';
|
||||
|
||||
import {
|
||||
createGlobalTimeStore,
|
||||
defaultGlobalTimeStore,
|
||||
GlobalTimeStoreApi,
|
||||
} from './globalTimeStore';
|
||||
import { GlobalTimeProviderOptions, GlobalTimeSelectedTime } from './types';
|
||||
import { usePersistence } from './usePersistence';
|
||||
import { useQueryCacheSync } from './useQueryCacheSync';
|
||||
import { useUrlSync } from './useUrlSync';
|
||||
import { useComputedMinMaxSync } from 'store/globalTime/useComputedMinMaxSync';
|
||||
|
||||
export const GlobalTimeContext = createContext<GlobalTimeStoreApi | null>(null);
|
||||
|
||||
export function GlobalTimeProvider({
|
||||
children,
|
||||
name,
|
||||
inheritGlobalTime = false,
|
||||
initialTime,
|
||||
enableUrlParams = false,
|
||||
removeQueryParamsOnUnmount = false,
|
||||
localStoragePersistKey,
|
||||
refreshInterval: initialRefreshInterval,
|
||||
}: GlobalTimeProviderOptions & { children: ReactNode }): JSX.Element {
|
||||
const parentStore = useContext(GlobalTimeContext);
|
||||
const globalStore = parentStore ?? defaultGlobalTimeStore;
|
||||
|
||||
const resolveInitialTime = (): GlobalTimeSelectedTime => {
|
||||
if (inheritGlobalTime) {
|
||||
return globalStore.getState().selectedTime;
|
||||
}
|
||||
if (localStoragePersistKey) {
|
||||
const stored = get(localStoragePersistKey);
|
||||
if (stored) {
|
||||
return stored as GlobalTimeSelectedTime;
|
||||
}
|
||||
}
|
||||
return initialTime ?? DEFAULT_TIME_RANGE;
|
||||
};
|
||||
|
||||
// Create isolated store (stable reference)
|
||||
const [store] = useState(() =>
|
||||
createGlobalTimeStore({
|
||||
name,
|
||||
selectedTime: resolveInitialTime(),
|
||||
refreshInterval: initialRefreshInterval ?? 0,
|
||||
}),
|
||||
);
|
||||
|
||||
useComputedMinMaxSync(store);
|
||||
useQueryCacheSync(store);
|
||||
useUrlSync(store, enableUrlParams, removeQueryParamsOnUnmount);
|
||||
usePersistence(store, localStoragePersistKey);
|
||||
|
||||
return (
|
||||
<GlobalTimeContext.Provider value={store}>
|
||||
{children}
|
||||
</GlobalTimeContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,693 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { ReactNode } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
|
||||
import set from 'api/browser/localstorage/set';
|
||||
|
||||
import { GlobalTimeProvider } from '../GlobalTimeContext';
|
||||
import { useGlobalTime } from '../hooks';
|
||||
import { GlobalTimeProviderOptions } from '../types';
|
||||
import { createCustomTimeRange, NANO_SECOND_MULTIPLIER } from '../utils';
|
||||
|
||||
jest.mock('api/browser/localstorage/set');
|
||||
|
||||
const createTestQueryClient = (): QueryClient =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createWrapper = (
|
||||
providerProps: GlobalTimeProviderOptions,
|
||||
nuqsProps?: { searchParams?: string },
|
||||
) => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
return function Wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter searchParams={nuqsProps?.searchParams}>
|
||||
<GlobalTimeProvider {...providerProps}>{children}</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
describe('GlobalTimeProvider', () => {
|
||||
describe('name prop', () => {
|
||||
it('should pass name to store when provided', () => {
|
||||
const wrapper = createWrapper({ name: 'test-drawer' });
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.name), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current).toBe('test-drawer');
|
||||
});
|
||||
|
||||
it('should have undefined name when not provided', () => {
|
||||
const wrapper = createWrapper({});
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.name), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('store isolation', () => {
|
||||
it('should create isolated store for each provider', () => {
|
||||
const wrapper1 = createWrapper({ initialTime: '1h' });
|
||||
const wrapper2 = createWrapper({ initialTime: '15m' });
|
||||
|
||||
const { result: result1 } = renderHook(
|
||||
() => useGlobalTime((s) => s.selectedTime),
|
||||
{ wrapper: wrapper1 },
|
||||
);
|
||||
const { result: result2 } = renderHook(
|
||||
() => useGlobalTime((s) => s.selectedTime),
|
||||
{ wrapper: wrapper2 },
|
||||
);
|
||||
|
||||
expect(result1.current).toBe('1h');
|
||||
expect(result2.current).toBe('15m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('inheritGlobalTime', () => {
|
||||
it('should inherit time from parent store when inheritGlobalTime is true', () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const NestedWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter>
|
||||
<GlobalTimeProvider initialTime="6h">
|
||||
<GlobalTimeProvider inheritGlobalTime>{children}</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: NestedWrapper,
|
||||
});
|
||||
|
||||
// Should inherit '6h' from parent provider
|
||||
expect(result.current).toBe('6h');
|
||||
});
|
||||
|
||||
it('should use initialTime when inheritGlobalTime is false', () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const NestedWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter>
|
||||
<GlobalTimeProvider initialTime="6h">
|
||||
<GlobalTimeProvider inheritGlobalTime={false} initialTime="15m">
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: NestedWrapper,
|
||||
});
|
||||
|
||||
// Should use its own initialTime, not parent's
|
||||
expect(result.current).toBe('15m');
|
||||
});
|
||||
|
||||
it('should prefer URL params over inheritGlobalTime when both are present', async () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const NestedWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter searchParams="?relativeTime=1h">
|
||||
<GlobalTimeProvider initialTime="6h">
|
||||
<GlobalTimeProvider inheritGlobalTime enableUrlParams>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: NestedWrapper,
|
||||
});
|
||||
|
||||
// inheritGlobalTime sets initial value to '6h', but URL sync updates it to '1h'
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBe('1h');
|
||||
});
|
||||
});
|
||||
|
||||
it('should use inherited time when URL params are empty', async () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const NestedWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter searchParams="">
|
||||
<GlobalTimeProvider initialTime="6h">
|
||||
<GlobalTimeProvider inheritGlobalTime enableUrlParams>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: NestedWrapper,
|
||||
});
|
||||
|
||||
// No URL params, should keep inherited value
|
||||
expect(result.current).toBe('6h');
|
||||
});
|
||||
|
||||
it('should prefer custom time URL params over inheritGlobalTime', async () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
const startTime = 1700000000000;
|
||||
const endTime = 1700003600000;
|
||||
|
||||
const NestedWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={`?startTime=${startTime}&endTime=${endTime}`}
|
||||
>
|
||||
<GlobalTimeProvider initialTime="6h">
|
||||
<GlobalTimeProvider inheritGlobalTime enableUrlParams>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), {
|
||||
wrapper: NestedWrapper,
|
||||
});
|
||||
|
||||
// URL custom time params should override inherited time
|
||||
await waitFor(() => {
|
||||
const { minTime, maxTime } = result.current.getMinMaxTime();
|
||||
expect(minTime).toBe(startTime * NANO_SECOND_MULTIPLIER);
|
||||
expect(maxTime).toBe(endTime * NANO_SECOND_MULTIPLIER);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('URL sync', () => {
|
||||
it('should read relativeTime from URL on mount', async () => {
|
||||
const wrapper = createWrapper(
|
||||
{ enableUrlParams: true },
|
||||
{ searchParams: '?relativeTime=1h' },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBe('1h');
|
||||
});
|
||||
});
|
||||
|
||||
it('should read custom time from URL on mount', async () => {
|
||||
const startTime = 1700000000000;
|
||||
const endTime = 1700003600000;
|
||||
const wrapper = createWrapper(
|
||||
{ enableUrlParams: true },
|
||||
{ searchParams: `?startTime=${startTime}&endTime=${endTime}` },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
const { minTime, maxTime } = result.current.getMinMaxTime();
|
||||
expect(minTime).toBe(startTime * NANO_SECOND_MULTIPLIER);
|
||||
expect(maxTime).toBe(endTime * NANO_SECOND_MULTIPLIER);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use custom URL keys when provided', async () => {
|
||||
const wrapper = createWrapper(
|
||||
{
|
||||
enableUrlParams: {
|
||||
relativeTimeKey: 'modalTime',
|
||||
},
|
||||
},
|
||||
{ searchParams: '?modalTime=3h' },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBe('3h');
|
||||
});
|
||||
});
|
||||
|
||||
it('should use custom startTimeKey and endTimeKey when provided', async () => {
|
||||
const startTime = 1700000000000;
|
||||
const endTime = 1700003600000;
|
||||
const wrapper = createWrapper(
|
||||
{
|
||||
enableUrlParams: {
|
||||
startTimeKey: 'customStart',
|
||||
endTimeKey: 'customEnd',
|
||||
},
|
||||
},
|
||||
{ searchParams: `?customStart=${startTime}&customEnd=${endTime}` },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
const { minTime, maxTime } = result.current.getMinMaxTime();
|
||||
expect(minTime).toBe(startTime * NANO_SECOND_MULTIPLIER);
|
||||
expect(maxTime).toBe(endTime * NANO_SECOND_MULTIPLIER);
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT read from URL when enableUrlParams is false', async () => {
|
||||
const wrapper = createWrapper(
|
||||
{ enableUrlParams: false, initialTime: '15m' },
|
||||
{ searchParams: '?relativeTime=1h' },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Should use initialTime, not URL value
|
||||
expect(result.current).toBe('15m');
|
||||
});
|
||||
|
||||
it('should prefer startTime/endTime over relativeTime when both present in URL', async () => {
|
||||
const startTime = 1700000000000;
|
||||
const endTime = 1700003600000;
|
||||
const wrapper = createWrapper(
|
||||
{ enableUrlParams: true },
|
||||
{
|
||||
searchParams: `?relativeTime=15m&startTime=${startTime}&endTime=${endTime}`,
|
||||
},
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
const { minTime, maxTime } = result.current.getMinMaxTime();
|
||||
// Should use startTime/endTime, not relativeTime
|
||||
expect(minTime).toBe(startTime * NANO_SECOND_MULTIPLIER);
|
||||
expect(maxTime).toBe(endTime * NANO_SECOND_MULTIPLIER);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use initialTime when URL has invalid time values', async () => {
|
||||
const wrapper = createWrapper(
|
||||
{ enableUrlParams: true, initialTime: '15m' },
|
||||
{ searchParams: '?startTime=invalid&endTime=also-invalid' },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// parseAsInteger returns null for invalid values, so should fallback to initialTime
|
||||
expect(result.current).toBe('15m');
|
||||
});
|
||||
|
||||
it('should update store when custom time is set from URL with only startTime and endTime', async () => {
|
||||
const startTime = 1700000000000;
|
||||
const endTime = 1700003600000;
|
||||
const wrapper = createWrapper(
|
||||
{ enableUrlParams: true },
|
||||
{ searchParams: `?startTime=${startTime}&endTime=${endTime}` },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
// Verify selectedTime is a custom time range string
|
||||
expect(result.current.selectedTime).toContain('||_||');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeQueryParamsOnUnmount', () => {
|
||||
const createUnmountTestWrapper = (
|
||||
getQueryString: () => string,
|
||||
setQueryString: (qs: string) => void,
|
||||
) => {
|
||||
return function TestWrapper({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const queryClient = createTestQueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={getQueryString()}
|
||||
onUrlUpdate={(event): void => {
|
||||
setQueryString(event.queryString);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
it('should remove URL params when provider unmounts with removeQueryParamsOnUnmount=true', async () => {
|
||||
let currentQueryString = 'relativeTime=1h';
|
||||
const TestWrapper = createUnmountTestWrapper(
|
||||
() => currentQueryString,
|
||||
(qs) => {
|
||||
currentQueryString = qs;
|
||||
},
|
||||
);
|
||||
|
||||
const { unmount } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestWrapper>
|
||||
<GlobalTimeProvider enableUrlParams removeQueryParamsOnUnmount>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</TestWrapper>
|
||||
),
|
||||
});
|
||||
|
||||
// Verify initial URL params are present
|
||||
expect(currentQueryString).toContain('relativeTime=1h');
|
||||
|
||||
// Unmount the provider
|
||||
unmount();
|
||||
|
||||
// URL params should be removed
|
||||
await waitFor(() => {
|
||||
expect(currentQueryString).not.toContain('relativeTime');
|
||||
expect(currentQueryString).not.toContain('startTime');
|
||||
expect(currentQueryString).not.toContain('endTime');
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT remove URL params when provider unmounts with removeQueryParamsOnUnmount=false', async () => {
|
||||
let currentQueryString = 'relativeTime=1h';
|
||||
const TestWrapper = createUnmountTestWrapper(
|
||||
() => currentQueryString,
|
||||
(qs) => {
|
||||
currentQueryString = qs;
|
||||
},
|
||||
);
|
||||
|
||||
const { unmount } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestWrapper>
|
||||
<GlobalTimeProvider enableUrlParams removeQueryParamsOnUnmount={false}>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</TestWrapper>
|
||||
),
|
||||
});
|
||||
|
||||
// Verify initial URL params are present
|
||||
expect(currentQueryString).toContain('relativeTime=1h');
|
||||
|
||||
// Unmount the provider
|
||||
unmount();
|
||||
|
||||
// Wait a tick to ensure cleanup effects would have run
|
||||
await waitFor(() => {
|
||||
// URL params should still be present
|
||||
expect(currentQueryString).toContain('relativeTime=1h');
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove custom time URL params on unmount', async () => {
|
||||
const startTime = 1700000000000;
|
||||
const endTime = 1700003600000;
|
||||
let currentQueryString = `startTime=${startTime}&endTime=${endTime}`;
|
||||
const TestWrapper = createUnmountTestWrapper(
|
||||
() => currentQueryString,
|
||||
(qs) => {
|
||||
currentQueryString = qs;
|
||||
},
|
||||
);
|
||||
|
||||
const { unmount } = renderHook(() => useGlobalTime(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestWrapper>
|
||||
<GlobalTimeProvider enableUrlParams removeQueryParamsOnUnmount>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</TestWrapper>
|
||||
),
|
||||
});
|
||||
|
||||
// Verify initial URL params are present
|
||||
expect(currentQueryString).toContain('startTime');
|
||||
expect(currentQueryString).toContain('endTime');
|
||||
|
||||
// Unmount the provider
|
||||
unmount();
|
||||
|
||||
// URL params should be removed
|
||||
await waitFor(() => {
|
||||
expect(currentQueryString).not.toContain('startTime');
|
||||
expect(currentQueryString).not.toContain('endTime');
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove custom URL key params on unmount', async () => {
|
||||
let currentQueryString = 'modalTime=3h';
|
||||
const TestWrapper = createUnmountTestWrapper(
|
||||
() => currentQueryString,
|
||||
(qs) => {
|
||||
currentQueryString = qs;
|
||||
},
|
||||
);
|
||||
|
||||
const { unmount } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestWrapper>
|
||||
<GlobalTimeProvider
|
||||
enableUrlParams={{
|
||||
relativeTimeKey: 'modalTime',
|
||||
}}
|
||||
removeQueryParamsOnUnmount
|
||||
>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</TestWrapper>
|
||||
),
|
||||
});
|
||||
|
||||
// Verify initial URL params are present
|
||||
expect(currentQueryString).toContain('modalTime=3h');
|
||||
|
||||
// Unmount the provider
|
||||
unmount();
|
||||
|
||||
// URL params should be removed
|
||||
await waitFor(() => {
|
||||
expect(currentQueryString).not.toContain('modalTime');
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT remove URL params when enableUrlParams is false', async () => {
|
||||
let currentQueryString = 'relativeTime=1h';
|
||||
const TestWrapper = createUnmountTestWrapper(
|
||||
() => currentQueryString,
|
||||
(qs) => {
|
||||
currentQueryString = qs;
|
||||
},
|
||||
);
|
||||
|
||||
const { unmount } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestWrapper>
|
||||
<GlobalTimeProvider enableUrlParams={false} removeQueryParamsOnUnmount>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</TestWrapper>
|
||||
),
|
||||
});
|
||||
|
||||
// Verify initial URL params are present
|
||||
expect(currentQueryString).toContain('relativeTime=1h');
|
||||
|
||||
// Unmount the provider
|
||||
unmount();
|
||||
|
||||
// Wait a tick
|
||||
await waitFor(() => {
|
||||
// URL params should still be present (enableUrlParams is false)
|
||||
expect(currentQueryString).toContain('relativeTime=1h');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('localStorage persistence', () => {
|
||||
const mockSet = set as jest.MockedFunction<typeof set>;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
mockSet.mockClear();
|
||||
mockSet.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('should read from localStorage on mount', () => {
|
||||
localStorage.setItem('test-time-key', '6h');
|
||||
|
||||
const wrapper = createWrapper({ localStoragePersistKey: 'test-time-key' });
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current).toBe('6h');
|
||||
});
|
||||
|
||||
it('should write to localStorage on selectedTime change', async () => {
|
||||
const wrapper = createWrapper({
|
||||
localStoragePersistKey: 'test-persist-key',
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
mockSet.mockClear();
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('12h');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSet).toHaveBeenCalledWith('test-persist-key', '12h');
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT write to localStorage when persistKey is undefined', async () => {
|
||||
const wrapper = createWrapper({ initialTime: '15m' });
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
mockSet.mockClear();
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('1h');
|
||||
});
|
||||
|
||||
// Wait a tick to ensure any async operations complete
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedTime).toBe('1h');
|
||||
});
|
||||
|
||||
expect(mockSet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should only write to localStorage when selectedTime changes, not other state', async () => {
|
||||
const wrapper = createWrapper({
|
||||
localStoragePersistKey: 'test-key',
|
||||
initialTime: '15m',
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
mockSet.mockClear();
|
||||
|
||||
// Change refreshInterval (not selectedTime)
|
||||
act(() => {
|
||||
result.current.setRefreshInterval(5000);
|
||||
});
|
||||
|
||||
// Wait to ensure subscription handler had a chance to run
|
||||
await waitFor(() => {
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
});
|
||||
|
||||
// Should NOT have written to localStorage for refreshInterval change
|
||||
expect(mockSet).not.toHaveBeenCalled();
|
||||
|
||||
// Now change selectedTime
|
||||
act(() => {
|
||||
result.current.setSelectedTime('1h');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSet).toHaveBeenCalledWith('test-key', '1h');
|
||||
});
|
||||
});
|
||||
|
||||
it('should fallback to initialTime when localStorage contains empty string', () => {
|
||||
localStorage.setItem('test-key', '');
|
||||
|
||||
const wrapper = createWrapper({
|
||||
localStoragePersistKey: 'test-key',
|
||||
initialTime: '15m',
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Empty string is falsy, should use initialTime
|
||||
expect(result.current).toBe('15m');
|
||||
});
|
||||
|
||||
it('should write custom time range to localStorage', async () => {
|
||||
const wrapper = createWrapper({
|
||||
localStoragePersistKey: 'test-custom-key',
|
||||
initialTime: '15m',
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
mockSet.mockClear();
|
||||
|
||||
const customTime = createCustomTimeRange(1000000000, 2000000000);
|
||||
act(() => {
|
||||
result.current.setSelectedTime(customTime);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSet).toHaveBeenCalledWith('test-custom-key', customTime);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshInterval', () => {
|
||||
it('should initialize with provided refreshInterval', () => {
|
||||
const wrapper = createWrapper({ refreshInterval: 5000 });
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
expect(result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
import { act } from '@testing-library/react';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
|
||||
import {
|
||||
createGlobalTimeStore,
|
||||
defaultGlobalTimeStore,
|
||||
} from '../globalTimeStore';
|
||||
import { createCustomTimeRange } from '../utils';
|
||||
|
||||
describe('createGlobalTimeStore', () => {
|
||||
describe('factory function', () => {
|
||||
it('should create independent store instances', () => {
|
||||
const store1 = createGlobalTimeStore();
|
||||
const store2 = createGlobalTimeStore();
|
||||
|
||||
store1.getState().setSelectedTime('1h');
|
||||
|
||||
expect(store1.getState().selectedTime).toBe('1h');
|
||||
expect(store2.getState().selectedTime).toBe(DEFAULT_TIME_RANGE);
|
||||
});
|
||||
|
||||
it('should accept initial state', () => {
|
||||
const store = createGlobalTimeStore({
|
||||
selectedTime: '15m',
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
|
||||
expect(store.getState().selectedTime).toBe('15m');
|
||||
expect(store.getState().refreshInterval).toBe(5000);
|
||||
expect(store.getState().isRefreshEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should compute isRefreshEnabled correctly for custom time', () => {
|
||||
const customTime = createCustomTimeRange(1000000000, 2000000000);
|
||||
const store = createGlobalTimeStore({
|
||||
selectedTime: customTime,
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
|
||||
expect(store.getState().isRefreshEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultGlobalTimeStore', () => {
|
||||
it('should be a singleton', () => {
|
||||
expect(defaultGlobalTimeStore).toBeDefined();
|
||||
expect(defaultGlobalTimeStore.getState().selectedTime).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setRefreshInterval', () => {
|
||||
it('should update refresh interval and enable refresh', () => {
|
||||
const store = createGlobalTimeStore();
|
||||
|
||||
act(() => {
|
||||
store.getState().setRefreshInterval(10000);
|
||||
});
|
||||
|
||||
expect(store.getState().refreshInterval).toBe(10000);
|
||||
expect(store.getState().isRefreshEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should disable refresh when interval is 0', () => {
|
||||
const store = createGlobalTimeStore({ refreshInterval: 5000 });
|
||||
|
||||
act(() => {
|
||||
store.getState().setRefreshInterval(0);
|
||||
});
|
||||
|
||||
expect(store.getState().refreshInterval).toBe(0);
|
||||
expect(store.getState().isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should not enable refresh for custom time range', () => {
|
||||
const customTime = createCustomTimeRange(1000000000, 2000000000);
|
||||
const store = createGlobalTimeStore({ selectedTime: customTime });
|
||||
|
||||
act(() => {
|
||||
store.getState().setRefreshInterval(10000);
|
||||
});
|
||||
|
||||
expect(store.getState().refreshInterval).toBe(10000);
|
||||
expect(store.getState().isRefreshEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('store name', () => {
|
||||
it('should store name when provided', () => {
|
||||
const store = createGlobalTimeStore({ name: 'drawer' });
|
||||
expect(store.getState().name).toBe('drawer');
|
||||
});
|
||||
|
||||
it('should have undefined name when not provided', () => {
|
||||
const store = createGlobalTimeStore();
|
||||
expect(store.getState().name).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAutoRefreshQueryKey', () => {
|
||||
it('should generate key without name for unnamed store', () => {
|
||||
const store = createGlobalTimeStore();
|
||||
const key = store
|
||||
.getState()
|
||||
.getAutoRefreshQueryKey('15m', 'MY_QUERY', 'param1');
|
||||
|
||||
expect(key).toStrictEqual([
|
||||
REACT_QUERY_KEY.AUTO_REFRESH_QUERY,
|
||||
'MY_QUERY',
|
||||
'param1',
|
||||
'15m',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate key with name for named store', () => {
|
||||
const store = createGlobalTimeStore({ name: 'drawer' });
|
||||
const key = store
|
||||
.getState()
|
||||
.getAutoRefreshQueryKey('15m', 'MY_QUERY', 'param1');
|
||||
|
||||
expect(key).toStrictEqual([
|
||||
REACT_QUERY_KEY.AUTO_REFRESH_QUERY,
|
||||
'drawer',
|
||||
'MY_QUERY',
|
||||
'param1',
|
||||
'15m',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle no query parts for named store', () => {
|
||||
const store = createGlobalTimeStore({ name: 'test' });
|
||||
const key = store.getState().getAutoRefreshQueryKey('1h');
|
||||
|
||||
expect(key).toStrictEqual([
|
||||
REACT_QUERY_KEY.AUTO_REFRESH_QUERY,
|
||||
'test',
|
||||
'1h',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle no query parts for unnamed store', () => {
|
||||
const store = createGlobalTimeStore();
|
||||
const key = store.getState().getAutoRefreshQueryKey('1h');
|
||||
|
||||
expect(key).toStrictEqual([REACT_QUERY_KEY.AUTO_REFRESH_QUERY, '1h']);
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user