Compare commits
17 Commits
ns/scope-2
...
chore/dash
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18db419644 | ||
|
|
a6e09083d7 | ||
|
|
e97d127af2 | ||
|
|
fbbceffbeb | ||
|
|
74bbcb20d5 | ||
|
|
bdb8b6a675 | ||
|
|
b5a1fb2106 | ||
|
|
485aed0e1a | ||
|
|
4d69e3f9e5 | ||
|
|
dca6aa497d | ||
|
|
bb47550c01 | ||
|
|
ecf510ed67 | ||
|
|
5b94d79e46 | ||
|
|
60a6a5b38c | ||
|
|
b47245d46a | ||
|
|
52ec7bf128 | ||
|
|
afe62d77b0 |
21
.github/CODEOWNERS
vendored
@@ -152,32 +152,21 @@ go.mod @therealpandey
|
||||
|
||||
## Dashboard Types
|
||||
|
||||
/frontend/src/api/types/dashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/types/api/dashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/types/api/widgets/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard List
|
||||
## Widget Card
|
||||
|
||||
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
|
||||
|
||||
# Dashboard Widget Page
|
||||
|
||||
/frontend/src/pages/DashboardWidget/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard Page
|
||||
|
||||
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/WidgetCard/ @SigNoz/pulse-frontend
|
||||
|
||||
## Public Dashboard Page
|
||||
|
||||
/frontend/src/pages/PublicDashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/PublicDashboardContainer/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard Libs + Components
|
||||
|
||||
/frontend/src/lib/uPlotV2/ @SigNoz/pulse-frontend
|
||||
/frontend/src/lib/visualization/ @SigNoz/pulse-frontend
|
||||
/frontend/src/lib/dashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/lib/dashboardVariables/ @SigNoz/pulse-frontend
|
||||
/frontend/src/components/NewSelect/ @SigNoz/pulse-frontend
|
||||
|
||||
@@ -8024,13 +8024,6 @@ components:
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
ServiceaccounttypesGettableFactorAPIKey:
|
||||
properties:
|
||||
createdAt:
|
||||
@@ -8807,7 +8800,6 @@ components:
|
||||
- span
|
||||
- trace
|
||||
- resource
|
||||
- scope
|
||||
- attribute
|
||||
- body
|
||||
- ""
|
||||
@@ -13263,117 +13255,6 @@ paths:
|
||||
summary: Gets service account roles
|
||||
tags:
|
||||
- serviceaccount
|
||||
post:
|
||||
deprecated: true
|
||||
description: This endpoint assigns a role to a service account
|
||||
operationId: CreateServiceAccountRoleDeprecated
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ServiceaccounttypesDeprecatedPostableServiceAccountRole'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/TypesIdentifiable'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: Created
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- serviceaccount:attach
|
||||
- role:attach
|
||||
- tokenizer:
|
||||
- serviceaccount:attach
|
||||
- role:attach
|
||||
summary: Create service account role
|
||||
tags:
|
||||
- serviceaccount
|
||||
/api/v1/service_accounts/{id}/roles/{rid}:
|
||||
delete:
|
||||
deprecated: true
|
||||
description: This endpoint revokes a role from service account
|
||||
operationId: DeleteServiceAccountRoleDeprecated
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: rid
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- serviceaccount:detach
|
||||
- role:detach
|
||||
- tokenizer:
|
||||
- serviceaccount:detach
|
||||
- role:detach
|
||||
summary: Delete service account role
|
||||
tags:
|
||||
- serviceaccount
|
||||
/api/v1/service_accounts/me:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -14377,9 +14258,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- auth-domain:list
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- auth-domain:list
|
||||
summary: List all auth domains
|
||||
tags:
|
||||
- authdomains
|
||||
@@ -14439,9 +14320,13 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- auth-domain:create
|
||||
- auth-domain:attach
|
||||
- role:attach
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- auth-domain:create
|
||||
- auth-domain:attach
|
||||
- role:attach
|
||||
summary: Create auth domain
|
||||
tags:
|
||||
- authdomains
|
||||
@@ -14485,9 +14370,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- auth-domain:delete
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- auth-domain:delete
|
||||
summary: Delete auth domain
|
||||
tags:
|
||||
- authdomains
|
||||
@@ -14542,9 +14427,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- auth-domain:read
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- auth-domain:read
|
||||
summary: Get auth domain by ID
|
||||
tags:
|
||||
- authdomains
|
||||
@@ -14598,9 +14483,17 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- auth-domain:update
|
||||
- auth-domain:attach
|
||||
- auth-domain:detach
|
||||
- role:attach
|
||||
- role:detach
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- auth-domain:update
|
||||
- auth-domain:attach
|
||||
- auth-domain:detach
|
||||
- role:attach
|
||||
- role:detach
|
||||
summary: Update auth domain
|
||||
tags:
|
||||
- authdomains
|
||||
|
||||
@@ -94,17 +94,12 @@ export const OnboardingV2 = Loadable(
|
||||
export const DashboardsListPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "DashboardsListPage" */ 'pages/DashboardsListPage'
|
||||
/* webpackChunkName: "DashboardsListPage" */ 'pages/DashboardsListPageV2'
|
||||
),
|
||||
);
|
||||
|
||||
export const DashboardPage = Loadable(
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPage'),
|
||||
);
|
||||
|
||||
export const DashboardWidget = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "DashboardWidgetPage" */ 'pages/DashboardWidget'),
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPageV2'),
|
||||
);
|
||||
|
||||
export const DashboardPanelEditorPage = Loadable(
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
DashboardPage,
|
||||
DashboardPanelEditorPage,
|
||||
DashboardsListPage,
|
||||
DashboardWidget,
|
||||
EditRulesPage,
|
||||
ErrorDetails,
|
||||
ForgotPassword,
|
||||
@@ -183,13 +182,6 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: false,
|
||||
key: 'PUBLIC_DASHBOARD',
|
||||
},
|
||||
{
|
||||
path: ROUTES.DASHBOARD_WIDGET,
|
||||
exact: true,
|
||||
component: DashboardWidget,
|
||||
isPrivate: true,
|
||||
key: 'DASHBOARD_WIDGET',
|
||||
},
|
||||
{
|
||||
path: ROUTES.DASHBOARD_PANEL_EDITOR,
|
||||
exact: true,
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { CreatePublicDashboardProps } from 'types/api/dashboard/public/create';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useCreatePublicDashboard` hook (or `createPublicDashboard` fetcher) from
|
||||
* `api/generated/services/dashboard` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
const createPublicDashboard = async (
|
||||
props: CreatePublicDashboardProps,
|
||||
): Promise<SuccessResponseV2<CreatePublicDashboardProps>> => {
|
||||
|
||||
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = DEFAULT_TIME_RANGE } = props;
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`/dashboards/${dashboardId}/public`,
|
||||
{ timeRangeEnabled, defaultTimeRange },
|
||||
);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default createPublicDashboard;
|
||||
@@ -1,27 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { GetPublicDashboardDataProps, PayloadProps,PublicDashboardDataProps } from 'types/api/dashboard/public/get';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useGetPublicDashboardData` hook (or `getPublicDashboardData` fetcher) from
|
||||
* `api/generated/services/dashboard` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
const getPublicDashboardData = async (props: GetPublicDashboardDataProps): Promise<SuccessResponseV2<PublicDashboardDataProps>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/public/dashboards/${props.id}`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default getPublicDashboardData;
|
||||
@@ -1,27 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { GetPublicDashboardMetaProps, PayloadProps,PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useGetPublicDashboard` hook (or `getPublicDashboard` fetcher) from
|
||||
* `api/generated/services/dashboard` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
const getPublicDashboardMeta = async (props: GetPublicDashboardMetaProps): Promise<SuccessResponseV2<PublicDashboardMetaProps>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}/public`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default getPublicDashboardMeta;
|
||||
@@ -1,34 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { MetricRangePayloadV5 } from 'api/v5/v5';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { GetPublicDashboardWidgetDataProps } from 'types/api/dashboard/public/getWidgetData';
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useGetPublicDashboardWidgetQueryRange` hook (or `getPublicDashboardWidgetQueryRange` fetcher) from
|
||||
* `api/generated/services/dashboard` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
const getPublicDashboardWidgetData = async (props: GetPublicDashboardWidgetDataProps): Promise<SuccessResponseV2<MetricRangePayloadV5>> => {
|
||||
try {
|
||||
const response = await axios.get(`/public/dashboards/${props.id}/widgets/${props.index}/query_range`, {
|
||||
params: {
|
||||
startTime: props.startTime,
|
||||
endTime: props.endTime,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default getPublicDashboardWidgetData;
|
||||
@@ -1,29 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps,RevokePublicDashboardAccessProps } from 'types/api/dashboard/public/delete';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useDeletePublicDashboard` hook (or `deletePublicDashboard` fetcher) from
|
||||
* `api/generated/services/dashboard` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
const revokePublicDashboardAccess = async (
|
||||
props: RevokePublicDashboardAccessProps,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.delete<PayloadProps>(`/dashboards/${props.id}/public`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default revokePublicDashboardAccess;
|
||||
@@ -1,36 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { UpdatePublicDashboardProps } from 'types/api/dashboard/public/update';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useUpdatePublicDashboard` hook (or `updatePublicDashboard` fetcher) from
|
||||
* `api/generated/services/dashboard` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
const updatePublicDashboard = async (
|
||||
props: UpdatePublicDashboardProps,
|
||||
): Promise<SuccessResponseV2<UpdatePublicDashboardProps>> => {
|
||||
|
||||
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = DEFAULT_TIME_RANGE } = props;
|
||||
|
||||
try {
|
||||
const response = await axios.put(
|
||||
`/dashboards/${dashboardId}/public`,
|
||||
{ timeRangeEnabled, defaultTimeRange },
|
||||
);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default updatePublicDashboard;
|
||||
@@ -22,10 +22,7 @@ import type {
|
||||
CreateServiceAccountKey201,
|
||||
CreateServiceAccountKeyPathParameters,
|
||||
CreateServiceAccountRole201,
|
||||
CreateServiceAccountRoleDeprecated201,
|
||||
CreateServiceAccountRoleDeprecatedPathParameters,
|
||||
DeleteServiceAccountPathParameters,
|
||||
DeleteServiceAccountRoleDeprecatedPathParameters,
|
||||
DeleteServiceAccountRolePathParameters,
|
||||
GetMyServiceAccount200,
|
||||
GetServiceAccount200,
|
||||
@@ -39,7 +36,6 @@ import type {
|
||||
ListServiceAccounts200,
|
||||
RenderErrorResponseDTO,
|
||||
RevokeServiceAccountKeyPathParameters,
|
||||
ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO,
|
||||
ServiceaccounttypesPostableFactorAPIKeyDTO,
|
||||
ServiceaccounttypesPostableServiceAccountDTO,
|
||||
ServiceaccounttypesPostableServiceAccountRoleDTO,
|
||||
@@ -1253,194 +1249,6 @@ export const invalidateGetServiceAccountRoles = async (
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint assigns a role to a service account
|
||||
* @deprecated
|
||||
* @summary Create service account role
|
||||
*/
|
||||
export const createServiceAccountRoleDeprecated = (
|
||||
{ id }: CreateServiceAccountRoleDeprecatedPathParameters,
|
||||
serviceaccounttypesDeprecatedPostableServiceAccountRoleDTO?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateServiceAccountRoleDeprecated201>({
|
||||
url: `/api/v1/service_accounts/${id}/roles`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: serviceaccounttypesDeprecatedPostableServiceAccountRoleDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateServiceAccountRoleDeprecatedMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
|
||||
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
|
||||
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createServiceAccountRoleDeprecated'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
|
||||
{
|
||||
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
|
||||
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return createServiceAccountRoleDeprecated(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateServiceAccountRoleDeprecatedMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>
|
||||
>;
|
||||
export type CreateServiceAccountRoleDeprecatedMutationBody =
|
||||
| BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>
|
||||
| undefined;
|
||||
export type CreateServiceAccountRoleDeprecatedMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Create service account role
|
||||
*/
|
||||
export const useCreateServiceAccountRoleDeprecated = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
|
||||
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
|
||||
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(
|
||||
getCreateServiceAccountRoleDeprecatedMutationOptions(options),
|
||||
);
|
||||
};
|
||||
/**
|
||||
* This endpoint revokes a role from service account
|
||||
* @deprecated
|
||||
* @summary Delete service account role
|
||||
*/
|
||||
export const deleteServiceAccountRoleDeprecated = (
|
||||
{ id, rid }: DeleteServiceAccountRoleDeprecatedPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/service_accounts/${id}/roles/${rid}`,
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteServiceAccountRoleDeprecatedMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteServiceAccountRoleDeprecated'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
|
||||
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteServiceAccountRoleDeprecated(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteServiceAccountRoleDeprecatedMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>
|
||||
>;
|
||||
|
||||
export type DeleteServiceAccountRoleDeprecatedMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Delete service account role
|
||||
*/
|
||||
export const useDeleteServiceAccountRoleDeprecated = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(
|
||||
getDeleteServiceAccountRoleDeprecatedMutationOptions(options),
|
||||
);
|
||||
};
|
||||
/**
|
||||
* This endpoint gets my service account
|
||||
* @summary Gets my service account
|
||||
|
||||
@@ -3492,7 +3492,6 @@ export enum TelemetrytypesFieldContextDTO {
|
||||
span = 'span',
|
||||
trace = 'trace',
|
||||
resource = 'resource',
|
||||
scope = 'scope',
|
||||
attribute = 'attribute',
|
||||
body = 'body',
|
||||
'' = '',
|
||||
@@ -9093,13 +9092,6 @@ export interface SavedviewtypesUpdatableSavedViewDTO {
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -11021,21 +11013,6 @@ export type GetServiceAccountRoles200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateServiceAccountRoleDeprecatedPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type CreateServiceAccountRoleDeprecated201 = {
|
||||
data: TypesIdentifiableDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteServiceAccountRoleDeprecatedPathParameters = {
|
||||
id: string;
|
||||
rid: string;
|
||||
};
|
||||
export type GetMyServiceAccount200 = {
|
||||
data: ServiceaccounttypesServiceAccountWithRolesDTO;
|
||||
/**
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/create';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
const create = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>('/dashboards', {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default create;
|
||||
@@ -1,19 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { Dashboard, PayloadProps } from 'types/api/dashboard/getAll';
|
||||
|
||||
const getAll = async (): Promise<SuccessResponseV2<Dashboard[]>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>('/dashboards');
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default getAll;
|
||||
@@ -1,21 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/delete';
|
||||
|
||||
const deleteDashboard = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<null>> => {
|
||||
try {
|
||||
const response = await axios.delete<PayloadProps>(`/dashboards/${props.id}`);
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: null,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default deleteDashboard;
|
||||
@@ -1,20 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/get';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
const get = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}`);
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default get;
|
||||
@@ -1,23 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/lockUnlock';
|
||||
|
||||
const lock = async (props: Props): Promise<SuccessResponseV2<null>> => {
|
||||
try {
|
||||
const response = await axios.put<PayloadProps>(
|
||||
`/dashboards/${props.id}/lock`,
|
||||
{ lock: props.lock },
|
||||
);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default lock;
|
||||
@@ -1,23 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/update';
|
||||
|
||||
const update = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
|
||||
try {
|
||||
const response = await axios.put<PayloadProps>(`/dashboards/${props.id}`, {
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default update;
|
||||
@@ -1,176 +0,0 @@
|
||||
export default function ApacheIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.7112 1.05739C8.3796 1.30831 8.08524 1.60498 7.83691 1.93853L8.17977 2.58567C8.40218 2.26279 8.64677 1.95576 8.91177 1.66681C8.93063 1.64581 8.94091 1.63596 8.94091 1.63596L8.91177 1.66681C8.66003 1.95965 8.43081 2.27111 8.22606 2.59853C8.67305 2.56672 9.11808 2.51165 9.55934 2.43353C9.60936 2.25525 9.62374 2.06886 9.60168 1.88502C9.57962 1.70118 9.52154 1.52349 9.43077 1.3621C9.43077 1.3621 9.09991 0.828957 8.7112 1.05739Z"
|
||||
fill="url(#paint0_linear_2061_4195)"
|
||||
/>
|
||||
<path
|
||||
d="M7.55932 6.49365L7.42432 6.51722L7.49332 6.50651C7.51432 6.50265 7.53703 6.49837 7.55932 6.49365Z"
|
||||
fill="#BE202E"
|
||||
/>
|
||||
<path
|
||||
opacity="0.35"
|
||||
d="M7.55932 6.49365L7.42432 6.51722L7.49332 6.50651C7.51432 6.50265 7.53703 6.49837 7.55932 6.49365Z"
|
||||
fill="#BE202E"
|
||||
/>
|
||||
<path
|
||||
d="M7.67407 5.92858L7.6955 5.92558C7.72464 5.9213 7.75336 5.91616 7.78122 5.91016L7.67493 5.92816L7.67407 5.92858Z"
|
||||
fill="#BE202E"
|
||||
/>
|
||||
<path
|
||||
opacity="0.35"
|
||||
d="M7.67407 5.92858L7.6955 5.92558C7.72464 5.9213 7.75336 5.91616 7.78122 5.91016L7.67493 5.92816L7.67407 5.92858Z"
|
||||
fill="#BE202E"
|
||||
/>
|
||||
<path
|
||||
d="M7.16872 4.25736C7.273 4.0625 7.37858 3.87221 7.48543 3.6865C7.59629 3.49393 7.70829 3.3075 7.82143 3.12721L7.84115 3.09507C7.95315 2.91793 8.066 2.74779 8.17972 2.58464L7.83686 1.9375L7.75886 2.03393C7.65986 2.15736 7.55743 2.29107 7.452 2.43036C7.33329 2.58893 7.21115 2.75779 7.08729 2.93564C6.97286 3.09979 6.85672 3.27164 6.74058 3.44993C6.64158 3.60121 6.54258 3.75721 6.444 3.91707L6.43286 3.93507L6.879 4.8145C6.97443 4.62707 7.071 4.44136 7.16872 4.25736Z"
|
||||
fill="url(#paint1_linear_2061_4195)"
|
||||
/>
|
||||
<path
|
||||
d="M5.13606 9.22519C5.07692 9.38748 5.01763 9.55305 4.95821 9.72191L4.95563 9.72919L4.93035 9.80076C4.89049 9.91476 4.85535 10.015 4.77563 10.2503C4.92444 10.3467 5.0416 10.4847 5.11249 10.6472C5.10283 10.4581 5.01907 10.2804 4.87935 10.1526C5.16043 10.1981 5.44862 10.1654 5.71236 10.0581C5.97609 9.95074 6.20521 9.77291 6.37463 9.54405C6.40102 9.50088 6.42464 9.45607 6.44535 9.40991C6.37459 9.49741 6.28141 9.56408 6.17575 9.6028C6.07008 9.64152 5.95589 9.65084 5.84535 9.62976C6.20937 9.49539 6.51802 9.24316 6.72221 8.91319C6.76978 8.83691 6.81563 8.75376 6.86278 8.66162C6.6974 8.84328 6.48756 8.97872 6.25393 9.05463C6.02029 9.13053 5.77091 9.14426 5.53035 9.09448L5.16949 9.13391C5.15706 9.16434 5.14721 9.19476 5.13606 9.22519Z"
|
||||
fill="url(#paint2_linear_2061_4195)"
|
||||
/>
|
||||
<path
|
||||
d="M5.30448 8.417C5.38248 8.21528 5.46276 8.01157 5.54533 7.80586C5.62419 7.60871 5.70519 7.41057 5.78833 7.21143C5.87148 7.01228 5.95633 6.81228 6.0429 6.61143C6.1309 6.40828 6.22076 6.20557 6.31248 6.00328C6.40419 5.801 6.49633 5.60257 6.5889 5.408C6.62262 5.33714 6.65662 5.26643 6.6909 5.19586C6.75005 5.07414 6.80962 4.95343 6.86962 4.83371C6.87262 4.82728 6.87605 4.82086 6.87948 4.81443L6.4329 3.93457L6.41105 3.97014C6.30733 4.14157 6.20362 4.313 6.10205 4.49128C6.00048 4.66957 5.89848 4.853 5.80205 5.03814C5.7189 5.19443 5.63776 5.35157 5.55862 5.50957L5.51148 5.606C5.41419 5.80614 5.32633 5.99943 5.24705 6.18543C5.15705 6.396 5.07762 6.59686 5.00876 6.788C4.9629 6.91357 4.92305 7.03486 4.88362 7.151C4.85233 7.25043 4.82276 7.34986 4.79448 7.451C4.72762 7.68471 4.67048 7.91771 4.62305 8.15L5.07133 9.035C5.13076 8.87671 5.19162 8.71614 5.2539 8.55328L5.30448 8.417Z"
|
||||
fill="url(#paint3_linear_2061_4195)"
|
||||
/>
|
||||
<path
|
||||
d="M4.615 8.18082C4.55868 8.46014 4.51975 8.74268 4.49843 9.02682C4.49843 9.03668 4.49843 9.04653 4.49629 9.05639C4.3547 8.8778 4.1801 8.72808 3.982 8.61539C4.24526 8.95089 4.41819 9.34823 4.48429 9.76953C4.28982 9.78674 4.0942 9.75338 3.91643 9.67268C4.05057 9.80984 4.21714 9.91096 4.40072 9.96668C4.14974 10.0146 3.9168 10.1307 3.72743 10.3022C3.97375 10.178 4.25059 10.1271 4.525 10.1557C4.21858 11.024 3.91129 11.9827 3.604 13.0001C3.64664 12.988 3.6856 12.9655 3.71739 12.9346C3.74918 12.9037 3.7728 12.8654 3.78615 12.8231C3.841 12.6388 4.20443 11.4298 4.77443 9.84068L4.82372 9.70439L4.83743 9.66625C4.89772 9.49968 4.96015 9.32968 5.02472 9.15625L5.06758 9.03753V9.03539L4.62143 8.15039C4.61929 8.16025 4.61672 8.17053 4.615 8.18082Z"
|
||||
fill="url(#paint4_linear_2061_4195)"
|
||||
/>
|
||||
<path
|
||||
d="M6.94843 4.89059L6.90986 4.96987C6.87129 5.04959 6.83214 5.13102 6.79243 5.21416C6.74957 5.30416 6.70671 5.3963 6.66386 5.49059C6.64157 5.53816 6.621 5.58573 6.59743 5.63416C6.53057 5.7793 6.46286 5.92916 6.39429 6.08373C6.30857 6.27402 6.22286 6.47173 6.13714 6.67687C6.054 6.87259 5.96971 7.07516 5.88429 7.28459C5.80314 7.48459 5.721 7.68973 5.63786 7.90002C5.56386 8.08888 5.48914 8.28273 5.41371 8.48159C5.40986 8.49145 5.40643 8.50087 5.403 8.51073C5.32814 8.70873 5.25257 8.91187 5.17629 9.12016L5.17114 9.1343L5.532 9.09487L5.51057 9.09102C6.039 8.98351 6.52026 8.7126 6.88629 8.31659C7.0686 8.1182 7.22683 7.89896 7.35771 7.66345C7.47147 7.46033 7.57252 7.25036 7.66029 7.03473C7.74386 6.83245 7.824 6.61387 7.90114 6.37645C7.79448 6.43086 7.68084 6.47037 7.56343 6.49388C7.54157 6.49859 7.52057 6.50287 7.49657 6.50673C7.47257 6.51059 7.45071 6.51445 7.42757 6.51745C7.80325 6.36305 8.10458 6.06924 8.26843 5.69759C8.12155 5.79773 7.95733 5.86966 7.78414 5.90973C7.75586 5.91616 7.72757 5.92087 7.69843 5.92516L7.677 5.92816C7.80484 5.87656 7.92565 5.80903 8.03657 5.72716C8.05843 5.71087 8.07943 5.69373 8.1 5.67573C8.13129 5.64873 8.16086 5.62045 8.18914 5.59002C8.20714 5.57116 8.22471 5.55145 8.24186 5.53087C8.28293 5.48179 8.32059 5.42996 8.35457 5.37573C8.36529 5.35859 8.376 5.34145 8.38629 5.32345C8.39957 5.29773 8.41243 5.27245 8.42486 5.24716C8.481 5.13402 8.526 5.03288 8.56157 4.94459C8.57957 4.90173 8.595 4.85888 8.60829 4.82116C8.61386 4.80616 8.619 4.79116 8.62371 4.7783C8.63786 4.73545 8.64943 4.69816 8.65843 4.66473C8.66941 4.62595 8.67828 4.58661 8.685 4.54688C8.67015 4.5586 8.65454 4.56934 8.63829 4.57902C8.4822 4.66169 8.31419 4.71953 8.14029 4.75045H8.13257L8.08157 4.75859L8.09057 4.75473L6.957 4.87902L6.94843 4.89059Z"
|
||||
fill="url(#paint5_linear_2061_4195)"
|
||||
/>
|
||||
<path
|
||||
d="M8.22475 2.59871C8.12404 2.75343 8.01389 2.92914 7.89561 3.12843L7.87675 3.16014C7.77446 3.33157 7.66604 3.52114 7.55147 3.72886C7.45261 3.90771 7.34989 4.10014 7.24332 4.30614C7.15018 4.48586 7.05418 4.67671 6.95532 4.87871L8.08889 4.75443C8.33914 4.65567 8.55501 4.48583 8.70989 4.26586C8.74804 4.211 8.78618 4.15357 8.82432 4.09443C8.94089 3.91271 9.05489 3.71257 9.15689 3.51371C9.25018 3.33322 9.33429 3.14813 9.40889 2.95914C9.44758 2.86102 9.48092 2.76087 9.50875 2.65914C9.52932 2.58028 9.54561 2.50571 9.55804 2.43457C9.11675 2.51236 8.67172 2.56715 8.22475 2.59871Z"
|
||||
fill="url(#paint6_linear_2061_4195)"
|
||||
/>
|
||||
<path
|
||||
d="M7.49234 6.50635C7.46963 6.5102 7.44648 6.51406 7.42334 6.51706C7.44648 6.51406 7.47134 6.5102 7.49234 6.50635Z"
|
||||
fill="#BE202E"
|
||||
/>
|
||||
<path
|
||||
opacity="0.35"
|
||||
d="M7.49234 6.50635C7.46963 6.5102 7.44648 6.51406 7.42334 6.51706C7.44648 6.51406 7.47134 6.5102 7.49234 6.50635Z"
|
||||
fill="#BE202E"
|
||||
/>
|
||||
<path
|
||||
d="M7.49234 6.50635C7.46963 6.5102 7.44648 6.51406 7.42334 6.51706C7.44648 6.51406 7.47134 6.5102 7.49234 6.50635Z"
|
||||
fill="url(#paint7_linear_2061_4195)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_2061_4195"
|
||||
x1="7.26579"
|
||||
y1="1.16584"
|
||||
x2="9.7782"
|
||||
y2="0.46843"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F69923" />
|
||||
<stop offset="0.312" stopColor="#F79A23" />
|
||||
<stop offset="0.838" stopColor="#E97826" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_2061_4195"
|
||||
x1="1.76109"
|
||||
y1="12.4382"
|
||||
x2="6.87606"
|
||||
y2="1.48267"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.323" stopColor="#9E2064" />
|
||||
<stop offset="0.63" stopColor="#C92037" />
|
||||
<stop offset="0.751" stopColor="#CD2335" />
|
||||
<stop offset="1" stopColor="#E97826" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_2061_4195"
|
||||
x1="3.47784"
|
||||
y1="11.6287"
|
||||
x2="6.5258"
|
||||
y2="5.10046"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#282662" />
|
||||
<stop offset="0.095" stopColor="#662E8D" />
|
||||
<stop offset="0.788" stopColor="#9F2064" />
|
||||
<stop offset="0.949" stopColor="#CD2032" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_2061_4195"
|
||||
x1="1.94596"
|
||||
y1="11.7748"
|
||||
x2="7.06094"
|
||||
y2="0.819304"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.323" stopColor="#9E2064" />
|
||||
<stop offset="0.63" stopColor="#C92037" />
|
||||
<stop offset="0.751" stopColor="#CD2335" />
|
||||
<stop offset="1" stopColor="#E97826" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint4_linear_2061_4195"
|
||||
x1="2.46768"
|
||||
y1="11.0455"
|
||||
x2="5.15578"
|
||||
y2="5.28798"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#282662" />
|
||||
<stop offset="0.095" stopColor="#662E8D" />
|
||||
<stop offset="0.788" stopColor="#9F2064" />
|
||||
<stop offset="0.949" stopColor="#CD2032" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint5_linear_2061_4195"
|
||||
x1="3.08048"
|
||||
y1="12.3044"
|
||||
x2="8.19546"
|
||||
y2="1.3489"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.323" stopColor="#9E2064" />
|
||||
<stop offset="0.63" stopColor="#C92037" />
|
||||
<stop offset="0.751" stopColor="#CD2335" />
|
||||
<stop offset="1" stopColor="#E97826" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint6_linear_2061_4195"
|
||||
x1="2.70679"
|
||||
y1="12.9581"
|
||||
x2="7.82195"
|
||||
y2="2.00223"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.323" stopColor="#9E2064" />
|
||||
<stop offset="0.63" stopColor="#C92037" />
|
||||
<stop offset="0.751" stopColor="#CD2335" />
|
||||
<stop offset="1" stopColor="#E97826" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint7_linear_2061_4195"
|
||||
x1="3.41759"
|
||||
y1="12.4619"
|
||||
x2="8.53277"
|
||||
y2="1.50629"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.323" stopColor="#9E2064" />
|
||||
<stop offset="0.63" stopColor="#C92037" />
|
||||
<stop offset="0.751" stopColor="#CD2335" />
|
||||
<stop offset="1" stopColor="#E97826" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
export default function DockerIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_2061_4220)">
|
||||
<path
|
||||
d="M13.574 5.82107C13.2652 5.60679 12.5575 5.52644 12.0042 5.63358C11.9398 5.09788 11.6439 4.62915 11.1292 4.21399L10.8332 3.99971L10.6274 4.30774C10.37 4.70951 10.2413 5.27198 10.2799 5.80768C10.2928 5.99517 10.3571 6.32998 10.5502 6.62461C10.37 6.73175 9.99686 6.86567 9.50789 6.86567H0.204685L0.17895 6.97281C0.0888774 7.5085 0.0888774 9.18254 1.14401 10.4682C1.9418 11.4458 3.12561 11.9414 4.68258 11.9414C8.05386 11.9414 10.5502 10.3209 11.7211 7.38797C12.1843 7.40136 13.1751 7.38797 13.677 6.38354C13.6898 6.35676 13.7156 6.30319 13.8056 6.10231L13.8571 5.99517L13.574 5.82107ZM7.6421 2.04443H6.22668V3.38367H7.6421V2.04443ZM7.6421 3.65151H6.22668V4.99074H7.6421V3.65151ZM5.96933 3.65151H4.5539V4.99074H5.96933V3.65151ZM4.29655 3.65151H2.88113V4.99074H4.29655V3.65151ZM2.62378 5.25859H1.20835V6.59782H2.62378V5.25859ZM4.29655 5.25859H2.88113V6.59782H4.29655V5.25859ZM5.96933 5.25859H4.5539V6.59782H5.96933V5.25859ZM7.6421 5.25859H6.22668V6.59782H7.6421V5.25859ZM9.31488 5.25859H7.89945V6.59782H9.31488V5.25859Z"
|
||||
fill="#2396ED"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2061_4220">
|
||||
<rect
|
||||
width="13.7143"
|
||||
height="13.7143"
|
||||
fill="white"
|
||||
transform="translate(0.142822 0.143066)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
export default function ElasticSearchIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.38041 6.94508L9.70241 8.45858L13.0524 5.52158C13.1019 5.27892 13.1255 5.03171 13.1229 4.78408C13.1236 3.9846 12.8681 3.20592 12.3941 2.56216C11.92 1.9184 11.2522 1.44339 10.4885 1.20675C9.72487 0.970101 8.9055 0.984259 8.15047 1.24714C7.39544 1.51003 6.74446 2.00782 6.29291 2.66758L5.73291 5.56008L6.38041 6.94508Z"
|
||||
fill="#FED10A"
|
||||
/>
|
||||
<path
|
||||
d="M2.943 10.4593C2.89356 10.7062 2.86994 10.9575 2.8725 11.2093C2.87361 12.012 3.13177 12.7932 3.60915 13.4385C4.08654 14.0838 4.75804 14.5592 5.52528 14.7952C6.29252 15.0311 7.11514 15.0151 7.87262 14.7495C8.63009 14.4839 9.28259 13.9827 9.7345 13.3193L10.2845 10.4398L9.55 9.02928L6.215 7.50928L2.943 10.4593Z"
|
||||
fill="#24BBB1"
|
||||
/>
|
||||
<path
|
||||
d="M2.92394 4.71302L5.19994 5.25002L5.69994 2.66552C5.39077 2.43015 5.01365 2.30134 4.62509 2.29839C4.23654 2.29544 3.8575 2.41852 3.54479 2.64916C3.23208 2.8798 3.00256 3.20559 2.89063 3.57768C2.77869 3.94978 2.79039 4.34813 2.92394 4.71302Z"
|
||||
fill="#EF5098"
|
||||
/>
|
||||
<path
|
||||
d="M2.72503 5.2583C2.23266 5.42016 1.80253 5.73058 1.49379 6.14687C1.18505 6.56317 1.01286 7.0649 1.00091 7.58305C0.988962 8.1012 1.13784 8.61033 1.42706 9.04041C1.71628 9.4705 2.13164 9.80042 2.61603 9.9848L5.81003 7.0998L5.22653 5.8498L2.72503 5.2583Z"
|
||||
fill="#17A8E0"
|
||||
/>
|
||||
<path
|
||||
d="M10.312 13.3197C10.6209 13.5543 10.9975 13.6825 11.3853 13.6851C11.7732 13.6877 12.1514 13.5646 12.4634 13.3342C12.7755 13.1038 13.0044 12.7785 13.116 12.407C13.2276 12.0356 13.2159 11.6379 13.0825 11.2737L10.812 10.7412L10.312 13.3197Z"
|
||||
fill="#93C83E"
|
||||
/>
|
||||
<path
|
||||
d="M10.7735 10.145L13.2735 10.7285C13.7666 10.5672 14.1976 10.257 14.507 9.84058C14.8165 9.42415 14.9891 8.922 15.0013 8.40333C15.0134 7.88467 14.8643 7.375 14.5747 6.94457C14.2851 6.51414 13.8691 6.18413 13.384 6L10.1135 8.8665L10.7735 10.145Z"
|
||||
fill="#0779A1"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
function HerokuIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_2061_4245)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M13.6285 0H2.10787C1.31283 0 0.666748 0.644312 0.666748 1.44112V14.5602C0.666748 15.3557 1.31283 16 2.10787 16H13.6285C14.4237 16 15.0678 15.3557 15.0678 14.5602V1.44112C15.0678 0.644312 14.4237 0 13.6285 0ZM14.2677 14.5602C14.2677 14.9135 13.9815 15.1994 13.6285 15.1994H2.10787C1.75506 15.1994 1.46688 14.9135 1.46688 14.5602V1.44112C1.46688 1.08654 1.75506 0.800133 2.10787 0.800133H13.6285C13.9815 0.800133 14.2677 1.08654 14.2677 1.44112V14.5602ZM4.26699 13.6009L6.06823 12.0002L4.26699 10.3999V13.6009ZM10.7719 7.11485C10.4481 6.78927 9.8569 6.40038 8.86819 6.40038C7.78342 6.40038 6.66611 6.68302 5.86752 6.94177V2.40082H4.26704V9.33907L5.39807 8.82667C5.41688 8.81826 7.24025 8.00086 8.86819 8.00086C9.68027 8.00086 9.86022 8.44796 9.86885 8.82158V13.6009H11.4676V8.801C11.4693 8.6983 11.4592 7.81051 10.7719 7.11485ZM8.66761 5.00027H10.2681C10.9912 4.1811 11.3597 3.30903 11.4677 2.40066H9.86881C9.69063 3.30726 9.29643 4.17424 8.66761 5.00027Z"
|
||||
fill="#6762A6"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2061_4245">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default HerokuIcon;
|
||||
@@ -1,68 +0,0 @@
|
||||
export default function MongoDBIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.26568 13.0004L6.94382 12.8937C6.94382 12.8937 6.98668 11.2651 6.39739 11.1507C6.01168 10.7015 6.45439 -8.02403 7.86439 11.0868C7.59687 11.2225 7.39217 11.4564 7.29311 11.7395C7.24001 12.1577 7.23082 12.5803 7.26568 13.0004Z"
|
||||
fill="url(#paint0_linear_2061_4238)"
|
||||
/>
|
||||
<path
|
||||
d="M7.43957 11.4272C8.29654 10.7821 8.95282 9.90701 9.33214 8.90369C9.71147 7.90037 9.79826 6.81 9.58243 5.75931C8.95243 2.98002 7.46058 2.06631 7.29986 1.71745C7.1612 1.50022 7.04284 1.27068 6.94629 1.03174L7.065 8.7756C7.065 8.7756 6.819 11.1422 7.43957 11.4272Z"
|
||||
fill="url(#paint1_linear_2061_4238)"
|
||||
/>
|
||||
<path
|
||||
d="M6.78015 11.5296C6.78015 11.5296 4.15687 9.74286 4.30858 6.58214C4.32273 5.62928 4.54121 4.69054 4.94926 3.82935C5.3573 2.96816 5.94542 2.20456 6.67387 1.59014C6.759 1.51782 6.82663 1.42715 6.87168 1.32493C6.91674 1.22272 6.93805 1.11163 6.93401 1C7.0973 1.35143 7.07072 6.247 7.08787 6.81957C7.1543 9.04686 6.96401 11.1091 6.78015 11.5296Z"
|
||||
fill="url(#paint2_linear_2061_4238)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_2061_4238"
|
||||
x1="5.1021"
|
||||
y1="7.10853"
|
||||
x2="8.80138"
|
||||
y2="8.36386"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.231" stopColor="#999875" />
|
||||
<stop offset="0.563" stopColor="#9B9977" />
|
||||
<stop offset="0.683" stopColor="#A09F7E" />
|
||||
<stop offset="0.768" stopColor="#A9A889" />
|
||||
<stop offset="0.837" stopColor="#B7B69A" />
|
||||
<stop offset="0.896" stopColor="#C9C7B0" />
|
||||
<stop offset="0.948" stopColor="#DEDDCB" />
|
||||
<stop offset="0.994" stopColor="#F8F6EB" />
|
||||
<stop offset="1" stopColor="#FBF9EF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_2061_4238"
|
||||
x1="6.45855"
|
||||
y1="0.976404"
|
||||
x2="8.09399"
|
||||
y2="11.1888"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#48A547" />
|
||||
<stop offset="1" stopColor="#3F9143" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_2061_4238"
|
||||
x1="4.08293"
|
||||
y1="6.89501"
|
||||
x2="8.47176"
|
||||
y2="5.42519"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#41A247" />
|
||||
<stop offset="0.352" stopColor="#4BA74B" />
|
||||
<stop offset="0.956" stopColor="#67B554" />
|
||||
<stop offset="1" stopColor="#69B655" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
function NginxIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.97775 1H7.00561C7.14837 1.068 7.28743 1.14353 7.42218 1.22629C8.97332 2.11829 10.5245 3.01057 12.0756 3.90314C12.1356 3.93517 12.1846 3.9845 12.2163 4.04472C12.2479 4.10493 12.2607 4.17327 12.253 4.24086C12.2496 6.12186 12.253 8.00243 12.2509 9.88257C12.2307 9.9723 12.1759 10.0504 12.0983 10.0999C10.4489 11.0496 8.79932 11.9987 7.14961 12.9473C7.10963 12.9778 7.06144 12.9956 7.01125 12.9984C6.96106 13.0012 6.91118 12.9889 6.86803 12.9631C5.21661 12.0163 3.56675 11.0677 1.91846 10.1174C1.86489 10.0921 1.82003 10.0515 1.78952 10.0007C1.75901 9.94988 1.74423 9.89119 1.74703 9.832C1.74703 7.95143 1.74703 6.071 1.74703 4.19071C1.74299 4.13178 1.75661 4.07299 1.78616 4.02184C1.8157 3.97069 1.85983 3.92951 1.91289 3.90357C3.46203 3.01271 5.01118 2.12129 6.56032 1.22929C6.69832 1.15043 6.83375 1.06686 6.97775 1Z"
|
||||
fill="#019639"
|
||||
/>
|
||||
<path
|
||||
d="M3.90007 4.65924C3.90007 6.21039 3.90007 7.76167 3.90007 9.3131C3.89809 9.39901 3.91326 9.48446 3.94468 9.56445C3.9761 9.64444 4.02315 9.71736 4.08307 9.77896C4.19794 9.89243 4.34824 9.96309 4.5089 9.97916C4.66956 9.99522 4.83087 9.95572 4.96593 9.86724C5.05634 9.80581 5.13035 9.72321 5.18152 9.62662C5.23269 9.53003 5.25946 9.4224 5.2595 9.3131C5.2595 8.19024 5.25736 7.06739 5.2595 5.94453C6.28322 7.17024 7.30907 8.39424 8.33707 9.61653C8.4799 9.76122 8.65676 9.86772 8.85145 9.92628C9.04614 9.98484 9.25242 9.99357 9.45136 9.95167C9.59184 9.924 9.71973 9.85198 9.81624 9.74622C9.91275 9.64045 9.97278 9.50652 9.9875 9.3641C9.98979 7.78096 9.98979 6.19796 9.9875 4.6151C9.97275 4.44614 9.8952 4.28884 9.77016 4.17425C9.64512 4.05966 9.48168 3.99609 9.31207 3.99609C9.14247 3.99609 8.97902 4.05966 8.85399 4.17425C8.72895 4.28884 8.6514 4.44614 8.63665 4.6151C8.63665 5.75596 8.62979 6.89553 8.63665 8.03596C7.63122 6.85053 6.63822 5.65481 5.63665 4.4651C5.5046 4.29578 5.32979 4.16474 5.13023 4.08549C4.93067 4.00624 4.71359 3.98165 4.50136 4.01424C4.34059 4.03214 4.19154 4.10704 4.08124 4.22536C3.97093 4.34369 3.90666 4.49761 3.90007 4.65924Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default NginxIcon;
|
||||
@@ -1,60 +0,0 @@
|
||||
export default function RedisIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_2061_4282)">
|
||||
<path
|
||||
d="M13.3198 10.158C12.5879 10.5395 8.79654 12.0984 7.98938 12.5192C7.18221 12.94 6.73382 12.936 6.09616 12.6312C5.45855 12.3263 1.42388 10.6966 0.697072 10.3492C0.333858 10.1756 0.142822 10.029 0.142822 9.8906V8.50438C0.142822 8.50438 5.3955 7.3609 6.24348 7.05667C7.09141 6.75244 7.38563 6.74145 8.10723 7.00578C8.82895 7.2702 13.1439 8.0487 13.8571 8.30992L13.8568 9.67653C13.8569 9.81356 13.6923 9.96388 13.3198 10.158Z"
|
||||
fill="#912626"
|
||||
/>
|
||||
<path
|
||||
d="M13.3195 8.77972C12.5877 9.16104 8.79642 10.72 7.98926 11.1407C7.18215 11.5616 6.73376 11.5575 6.09615 11.2527C5.45849 10.9481 1.42397 9.31805 0.697221 8.97086C-0.029529 8.62345 -0.0447433 8.38436 0.66915 8.10482C1.38304 7.82518 5.39544 6.25098 6.24353 5.94675C7.09145 5.64263 7.38561 5.63154 8.10722 5.89597C8.82888 6.16029 12.5975 7.66034 13.3106 7.9215C14.024 8.18298 14.0513 8.39829 13.3195 8.77972Z"
|
||||
fill="#C6302B"
|
||||
/>
|
||||
<path
|
||||
d="M13.3198 7.91483C12.5879 8.29637 8.79654 9.85519 7.98938 10.2762C7.18221 10.6968 6.73382 10.6928 6.09616 10.388C5.4585 10.0833 1.42388 8.45338 0.697072 8.10597C0.333858 7.9324 0.142822 7.78604 0.142822 7.64756V6.26119C0.142822 6.26119 5.3955 5.11776 6.24348 4.81353C7.09141 4.50929 7.38563 4.49826 8.10723 4.76263C8.829 5.02701 13.144 5.80535 13.8571 6.06661L13.8568 7.43338C13.8569 7.57036 13.6923 7.72069 13.3198 7.91483Z"
|
||||
fill="#912626"
|
||||
/>
|
||||
<path
|
||||
d="M13.3195 6.53701C12.5877 6.91844 8.79642 8.47726 7.98926 8.89817C7.18215 9.31892 6.73376 9.3148 6.09615 9.00998C5.45849 8.70537 1.42397 7.07541 0.697221 6.72816C-0.029529 6.38085 -0.0447433 6.14171 0.66915 5.86207C1.38304 5.58258 5.39549 4.00828 6.24353 3.7041C7.09145 3.39992 7.38561 3.38889 8.10722 3.65326C8.82888 3.91758 12.5975 5.41753 13.3106 5.6788C14.024 5.94023 14.0513 6.15558 13.3195 6.53701Z"
|
||||
fill="#C6302B"
|
||||
/>
|
||||
<path
|
||||
d="M13.3198 5.58855C12.5879 5.96998 8.79654 7.5289 7.98938 7.94987C7.18221 8.37062 6.73382 8.36649 6.09616 8.06167C5.4585 7.75701 1.42388 6.12705 0.697072 5.7798C0.333858 5.60607 0.142822 5.45965 0.142822 5.32133V3.9349C0.142822 3.9349 5.3955 2.79153 6.24348 2.48735C7.09141 2.18307 7.38563 2.17214 8.10723 2.43646C8.829 2.70083 13.144 3.47917 13.8571 3.74044L13.8568 5.10715C13.8569 5.24403 13.6923 5.39435 13.3198 5.58855Z"
|
||||
fill="#912626"
|
||||
/>
|
||||
<path
|
||||
d="M13.3195 4.21078C12.5876 4.59221 8.79639 6.15113 7.98923 6.57188C7.18212 6.99263 6.73373 6.98851 6.09612 6.68385C5.45852 6.37903 1.42394 4.74917 0.697248 4.40187C-0.0295558 4.05462 -0.0447165 3.81542 0.669123 3.53583C1.38302 3.2563 5.39546 1.68221 6.2435 1.37792C7.09143 1.07369 7.38559 1.06276 8.1072 1.32714C8.82886 1.59151 12.5975 3.09146 13.3106 3.35272C14.0239 3.61394 14.0513 3.82935 13.3195 4.21078Z"
|
||||
fill="#C6302B"
|
||||
/>
|
||||
<path
|
||||
d="M8.67572 2.86218L7.49661 2.9846L7.23267 3.61974L6.80635 2.91099L5.44483 2.78863L6.46076 2.42226L6.15594 1.85986L7.1071 2.23186L8.00378 1.93829L7.76142 2.51981L8.67572 2.86218ZM7.16228 5.94351L4.96172 5.03081L8.11494 4.54679L7.16228 5.94351ZM4.11138 3.21522C5.04219 3.21522 5.79674 3.50772 5.79674 3.86847C5.79674 4.22933 5.04219 4.52177 4.11138 4.52177C3.18058 4.52177 2.42603 4.22927 2.42603 3.86847C2.42603 3.50772 3.18058 3.21522 4.11138 3.21522Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M10.0693 3.0357L11.9355 3.77316L10.0709 4.50993L10.0693 3.0357Z"
|
||||
fill="#621B1C"
|
||||
/>
|
||||
<path
|
||||
d="M8.00464 3.85234L10.0693 3.03564L10.0709 4.50988L9.86844 4.58906L8.00464 3.85234Z"
|
||||
fill="#9A2928"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2061_4282">
|
||||
<rect
|
||||
width="13.7143"
|
||||
height="13.7143"
|
||||
fill="white"
|
||||
transform="translate(0.142822 0.143066)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
3
frontend/src/assets/Logos/azure.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#0078D4" d="M22.379 23.343a1.62 1.62 0 0 0 1.536-2.14v.002L17.35 1.76A1.62 1.62 0 0 0 15.816.657H8.184A1.62 1.62 0 0 0 6.65 1.76L.086 21.204a1.62 1.62 0 0 0 1.536 2.139h4.741a1.62 1.62 0 0 0 1.535-1.103l.977-2.892 4.947 3.675c.28.208.618.32.966.32m-3.084-12.531 3.624 10.739a.54.54 0 0 1-.51.713v-.001h-.03a.54.54 0 0 1-.322-.106l-9.287-6.9h4.853m6.313 7.006c.116-.326.13-.694.007-1.058L9.79 1.76a1.722 1.722 0 0 0-.007-.02h6.034a.54.54 0 0 1 .512.366l6.562 19.445a.54.54 0 0 1-.338.684"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 583 B |
1
frontend/src/assets/Logos/cloudnative-pg.svg
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
5
frontend/src/assets/Logos/dify.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Dify</title>
|
||||
<path d="M7.043 6.487c1.635 0 2.241-1.003 2.241-2.243S8.681 2 7.044 2C5.405 2 4.801 3.003 4.801 4.244c0 1.24.604 2.243 2.241 2.243z" fill="#03F" />
|
||||
<path d="M14.883 6.97v1.443h-3.679v3.203h3.68v8.012H8.801V8.41h-8v3.203h4.48v8.012H0v3.203h24v-3.203h-5.6v-8.012H24V8.41h-5.6V5.206H24V2.003h-4.161a4.97 4.97 0 00-4.961 4.967h.005z" fill="#03F" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 447 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 115.28 122.88"><path d="M25.38 57h64.88V37.34H69.59c-2.17 0-5.19-1.17-6.62-2.6s-2.3-4.01-2.3-6.17V7.64H8.15c-.18 0-.32.09-.41.18-.15.1-.19.23-.19.42v106.45c0 .14.09.32.18.41.09.14.28.18.41.18h81.51c.18 0 .17-.09.27-.18.14-.09.33-.28.33-.41v-11.16H25.38c-4.14 0-7.56-3.4-7.56-7.56V64.55c0-4.15 3.4-7.55 7.56-7.55m4.11 11.38h7.43v18.15h11.63v5.92H29.49zm20.4 12.05c0-3.93 1.09-6.99 3.28-9.17 2.19-2.19 5.24-3.28 9.15-3.28 4.01 0 7.09 1.08 9.26 3.22 2.17 2.15 3.25 5.16 3.25 9.04 0 2.81-.47 5.11-1.42 6.91q-1.425 2.7-4.11 4.2t-6.69 1.5c-2.71 0-4.96-.43-6.74-1.29-1.78-.87-3.22-2.23-4.32-4.11-1.11-1.87-1.66-4.21-1.66-7.02m7.42.01c0 2.43.45 4.17 1.36 5.23s2.14 1.59 3.7 1.59c1.6 0 2.84-.52 3.71-1.56.88-1.04 1.32-2.9 1.32-5.6 0-2.26-.46-3.92-1.37-4.96-.92-1.05-2.16-1.57-3.73-1.57-1.5 0-2.71.53-3.62 1.59-.91 1.08-1.37 2.83-1.37 5.28m33.11 3.3v-5.01h11.49v10.23c-2.2 1.5-4.15 2.53-5.83 3.07-1.69.54-3.7.81-6.02.81-2.86 0-5.19-.49-6.99-1.46s-3.19-2.42-4.18-4.35c-.99-1.92-1.48-4.13-1.48-6.63 0-2.63.54-4.91 1.62-6.85s2.67-3.41 4.76-4.42c1.63-.78 3.83-1.17 6.58-1.17 2.66 0 4.64.24 5.96.72s2.41 1.23 3.28 2.24 1.52 2.3 1.96 3.85l-7.16 1.29c-.3-.91-.8-1.61-1.5-2.09-.71-.49-1.6-.73-2.7-.73-1.62 0-2.92.57-3.89 1.7s-1.45 2.92-1.45 5.37c0 2.6.49 4.46 1.47 5.57.97 1.11 2.34 1.68 4.09 1.68q1.245 0 2.37-.36c.75-.24 1.61-.65 2.59-1.22v-2.25h-4.97zM97.79 57h9.93c4.16 0 7.56 3.41 7.56 7.56v31.42c0 4.15-3.41 7.56-7.56 7.56h-9.93v13.55c0 1.61-.65 3.04-1.7 4.1a5.74 5.74 0 0 1-4.1 1.7H5.81a5.74 5.74 0 0 1-4.1-1.7 5.74 5.74 0 0 1-1.7-4.1V5.85c0-1.61.65-3.04 1.7-4.1a5.8 5.8 0 0 1 4.1-1.7h58.72c.13-.05.27-.05.41-.05.64 0 1.29.28 1.75.69h.09c.09.05.14.09.23.18L97 31.23c.51.51.88 1.2.88 1.98 0 .23-.05.41-.09.65zM67.52 27.97V8.94l21.43 21.7H70.19c-.74 0-1.38-.32-1.89-.78-.46-.46-.78-1.15-.78-1.89" style="fill-rule:evenodd;clip-rule:evenodd"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 115.28 122.88"><path d="M25.38 57h64.88V37.34H69.59c-2.17 0-5.19-1.17-6.62-2.6s-2.3-4.01-2.3-6.17V7.64H8.15c-.18 0-.32.09-.41.18-.15.1-.19.23-.19.42v106.45c0 .14.09.32.18.41.09.14.28.18.41.18h81.51c.18 0 .17-.09.27-.18.14-.09.33-.28.33-.41v-11.16H25.38c-4.14 0-7.56-3.4-7.56-7.56V64.55c0-4.15 3.4-7.55 7.56-7.55m4.11 11.38h7.43v18.15h11.63v5.92H29.49zm20.4 12.05c0-3.93 1.09-6.99 3.28-9.17 2.19-2.19 5.24-3.28 9.15-3.28 4.01 0 7.09 1.08 9.26 3.22 2.17 2.15 3.25 5.16 3.25 9.04 0 2.81-.47 5.11-1.42 6.91q-1.425 2.7-4.11 4.2t-6.69 1.5c-2.71 0-4.96-.43-6.74-1.29-1.78-.87-3.22-2.23-4.32-4.11-1.11-1.87-1.66-4.21-1.66-7.02m7.42.01c0 2.43.45 4.17 1.36 5.23s2.14 1.59 3.7 1.59c1.6 0 2.84-.52 3.71-1.56.88-1.04 1.32-2.9 1.32-5.6 0-2.26-.46-3.92-1.37-4.96-.92-1.05-2.16-1.57-3.73-1.57-1.5 0-2.71.53-3.62 1.59-.91 1.08-1.37 2.83-1.37 5.28m33.11 3.3v-5.01h11.49v10.23c-2.2 1.5-4.15 2.53-5.83 3.07-1.69.54-3.7.81-6.02.81-2.86 0-5.19-.49-6.99-1.46s-3.19-2.42-4.18-4.35c-.99-1.92-1.48-4.13-1.48-6.63 0-2.63.54-4.91 1.62-6.85s2.67-3.41 4.76-4.42c1.63-.78 3.83-1.17 6.58-1.17 2.66 0 4.64.24 5.96.72s2.41 1.23 3.28 2.24 1.52 2.3 1.96 3.85l-7.16 1.29c-.3-.91-.8-1.61-1.5-2.09-.71-.49-1.6-.73-2.7-.73-1.62 0-2.92.57-3.89 1.7s-1.45 2.92-1.45 5.37c0 2.6.49 4.46 1.47 5.57.97 1.11 2.34 1.68 4.09 1.68q1.245 0 2.37-.36c.75-.24 1.61-.65 2.59-1.22v-2.25h-4.97zM97.79 57h9.93c4.16 0 7.56 3.41 7.56 7.56v31.42c0 4.15-3.41 7.56-7.56 7.56h-9.93v13.55c0 1.61-.65 3.04-1.7 4.1a5.74 5.74 0 0 1-4.1 1.7H5.81a5.74 5.74 0 0 1-4.1-1.7 5.74 5.74 0 0 1-1.7-4.1V5.85c0-1.61.65-3.04 1.7-4.1a5.8 5.8 0 0 1 4.1-1.7h58.72c.13-.05.27-.05.41-.05.64 0 1.29.28 1.75.69h.09c.09.05.14.09.23.18L97 31.23c.51.51.88 1.2.88 1.98 0 .23-.05.41-.09.65zM67.52 27.97V8.94l21.43 21.7H70.19c-.74 0-1.38-.32-1.89-.78-.46-.46-.78-1.15-.78-1.89" style="fill:#D1D5DB;fill-rule:evenodd;clip-rule:evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
3
frontend/src/assets/Logos/github-copilot.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#9CA3AF" d="M23.922 16.997C23.061 18.492 18.063 22.02 12 22.02 5.937 22.02.939 18.492.078 16.997A.641.641 0 0 1 0 16.741v-2.869a.883.883 0 0 1 .053-.22c.372-.935 1.347-2.292 2.605-2.656.167-.429.414-1.055.644-1.517a10.098 10.098 0 0 1-.052-1.086c0-1.331.282-2.499 1.132-3.368.397-.406.89-.717 1.474-.952C7.255 2.937 9.248 1.98 11.978 1.98c2.731 0 4.767.957 6.166 2.093.584.235 1.077.546 1.474.952.85.869 1.132 2.037 1.132 3.368 0 .368-.014.733-.052 1.086.23.462.477 1.088.644 1.517 1.258.364 2.233 1.721 2.605 2.656a.841.841 0 0 1 .053.22v2.869a.641.641 0 0 1-.078.256Zm-11.75-5.992h-.344a4.359 4.359 0 0 1-.355.508c-.77.947-1.918 1.492-3.508 1.492-1.725 0-2.989-.359-3.782-1.259a2.137 2.137 0 0 1-.085-.104L4 11.746v6.585c1.435.779 4.514 2.179 8 2.179 3.486 0 6.565-1.4 8-2.179v-6.585l-.098-.104s-.033.045-.085.104c-.793.9-2.057 1.259-3.782 1.259-1.59 0-2.738-.545-3.508-1.492a4.359 4.359 0 0 1-.355-.508Zm2.328 3.25c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm-5 0c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm3.313-6.185c.136 1.057.403 1.913.878 2.497.442.544 1.134.938 2.344.938 1.573 0 2.292-.337 2.657-.751.384-.435.558-1.15.558-2.361 0-1.14-.243-1.847-.705-2.319-.477-.488-1.319-.862-2.824-1.025-1.487-.161-2.192.138-2.533.529-.269.307-.437.808-.438 1.578v.021c0 .265.021.562.063.893Zm-1.626 0c.042-.331.063-.628.063-.894v-.02c-.001-.77-.169-1.271-.438-1.578-.341-.391-1.046-.69-2.533-.529-1.505.163-2.347.537-2.824 1.025-.462.472-.705 1.179-.705 2.319 0 1.211.175 1.926.558 2.361.365.414 1.084.751 2.657.751 1.21 0 1.902-.394 2.344-.938.475-.584.742-1.44.878-2.497Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
frontend/src/assets/Logos/keda.svg
Normal file
|
After Width: | Height: | Size: 25 KiB |
24
frontend/src/assets/Logos/kong.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg viewBox="0 0 44.8 40" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="kong-a" x1="38.204" x2="8.732" y1="18.417" y2="48.543" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#11A06B" />
|
||||
<stop offset="1" stop-color="#286FEB" />
|
||||
</linearGradient>
|
||||
<linearGradient id="kong-b" x1="38.107" x2="8.635" y1="18.322" y2="48.448" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#11A06B" />
|
||||
<stop offset="1" stop-color="#286FEB" />
|
||||
</linearGradient>
|
||||
<linearGradient id="kong-c" x1="29.439" x2="-0.033" y1="9.842" y2="39.968" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#11A06B" />
|
||||
<stop offset="1" stop-color="#286FEB" />
|
||||
</linearGradient>
|
||||
<linearGradient id="kong-d" x1="30.291" x2="0.819" y1="10.676" y2="40.801" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#11A06B" />
|
||||
<stop offset="1" stop-color="#286FEB" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="m14.7 32.9-1.1 1.4 2.5 3.9-.3 1.8h10.6l.7-1.8-4.2-5.3z" fill="url(#kong-a)" />
|
||||
<path d="M20.5 9.4 16.7 16l18.6 22-.5 2h8.5l1.5-7.1L24.9 9.4z" fill="url(#kong-b)" />
|
||||
<path d="m23 4.4-1.8 3.3h4.5l7.7 9.2 4.6-3.8v-2.4l-1.6-2.2 1.2-1.2L28.4 0z" fill="url(#kong-c)" />
|
||||
<path d="M9.1 22.9H6.6L0 31.3V40h7.1l1.3-1.6 5.5-7.1h7.9l2.4-3.7-8.6-10.2z" fill="url(#kong-d)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
frontend/src/assets/Logos/neon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg fill="none" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M63 0.018v63.535L38.418 42.25v21.303H0V0l63 .018ZM7.723 55.839h22.972V25.323l24.583 21.725V7.729L7.723 7.716v48.123Z" fill="#37C38F" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 226 B |
@@ -8,7 +8,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { X } from '@signozhq/icons';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
|
||||
@@ -5,16 +5,16 @@ import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ViewMenuAction } from 'container/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { ViewMenuAction } from 'container/WidgetCard/config';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Card } from 'container/WidgetCard/styles';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
|
||||
|
||||
@@ -5,15 +5,15 @@ import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ViewMenuAction } from 'container/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { ViewMenuAction } from 'container/WidgetCard/config';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Card } from 'container/WidgetCard/styles';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
|
||||
import { Card } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { CardContainer } from 'container/GridCardLayout/styles';
|
||||
import { CardContainer } from 'container/WidgetCard/styles';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { ChevronDown, ChevronUp } from '@signozhq/icons';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||
import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@@ -6,9 +6,9 @@ import { Col, Row } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ViewMenuAction } from 'container/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { ViewMenuAction } from 'container/WidgetCard/config';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Card } from 'container/WidgetCard/styles';
|
||||
import { Button } from 'container/MetricsApplication/Tabs/styles';
|
||||
import { useGraphClickHandler } from 'container/MetricsApplication/Tabs/util';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { getQueryPayloadFromWidgetsData } from 'pages/Celery/CeleryOverview/CeleryOverviewUtils';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { History, Location } from 'history';
|
||||
import getRenderer from 'lib/uPlotLib/utils/getRenderer';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@@ -4,10 +4,9 @@ import { useSelector } from 'react-redux';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useUpdatedQuery from 'container/GridCardLayout/useResolveQuery';
|
||||
import useUpdatedQuery from 'container/WidgetCard/hooks/useResolveQuery';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
@@ -80,7 +79,6 @@ export function useNavigateToExplorer(): (
|
||||
);
|
||||
|
||||
const { getUpdatedQuery } = useUpdatedQuery();
|
||||
const { dashboardData } = useDashboardStore();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
return useCallback(
|
||||
@@ -112,7 +110,6 @@ export function useNavigateToExplorer(): (
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
},
|
||||
dashboardData,
|
||||
})
|
||||
.then((query) => {
|
||||
preparedQuery = query;
|
||||
@@ -136,13 +133,6 @@ export function useNavigateToExplorer(): (
|
||||
|
||||
window.open(withBasePath(newExplorerPath), sameTab ? '_self' : '_blank');
|
||||
},
|
||||
[
|
||||
prepareQuery,
|
||||
minTime,
|
||||
maxTime,
|
||||
getUpdatedQuery,
|
||||
dashboardData,
|
||||
notifications,
|
||||
],
|
||||
[prepareQuery, minTime, maxTime, getUpdatedQuery, notifications],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.client-side-qb-search {
|
||||
.ant-select-selection-search {
|
||||
width: max-content !important;
|
||||
}
|
||||
}
|
||||
@@ -1,661 +0,0 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
|
||||
import {
|
||||
KeyboardEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Select, Tooltip } from 'antd';
|
||||
import {
|
||||
OPERATORS,
|
||||
QUERY_BUILDER_OPERATORS_BY_TYPES,
|
||||
QUERY_BUILDER_SEARCH_VALUES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { CustomTagProps } from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import { selectStyle } from 'container/QueryBuilder/filters/QueryBuilderSearch/config';
|
||||
import { PLACEHOLDER } from 'container/QueryBuilder/filters/QueryBuilderSearch/constant';
|
||||
import { TypographyText } from 'container/QueryBuilder/filters/QueryBuilderSearch/style';
|
||||
import {
|
||||
checkCommaInValue,
|
||||
getOperatorFromValue,
|
||||
getOperatorValue,
|
||||
getTagToken,
|
||||
isInNInOperator,
|
||||
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import {
|
||||
DropdownState,
|
||||
ITag,
|
||||
Option,
|
||||
} from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import Suggestions from 'container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions';
|
||||
import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete';
|
||||
import { validationMapper } from 'hooks/queryBuilder/useIsValidTag';
|
||||
import { operatorTypeMapper } from 'hooks/queryBuilder/useOperatorType';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { isArray, isEmpty, isEqual, isObject } from 'lodash-es';
|
||||
import { ChevronDown, ChevronUp } from '@signozhq/icons';
|
||||
import type { BaseSelectRef } from 'rc-select';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import './ClientSideQBSearch.styles.scss';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
|
||||
export interface AttributeKey {
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface AttributeValuesMap {
|
||||
[key: string]: AttributeValue;
|
||||
}
|
||||
|
||||
interface ClientSideQBSearchProps {
|
||||
filters: TagFilter;
|
||||
onChange: (value: TagFilter) => void;
|
||||
whereClauseConfig?: WhereClauseConfig;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
suffixIcon?: React.ReactNode;
|
||||
attributeValuesMap?: AttributeValuesMap;
|
||||
attributeKeys: AttributeKey[];
|
||||
}
|
||||
|
||||
interface AttributeValue {
|
||||
stringAttributeValues: string[] | [];
|
||||
numberAttributeValues: number[] | [];
|
||||
boolAttributeValues: boolean[] | [];
|
||||
}
|
||||
|
||||
function ClientSideQBSearch(
|
||||
props: ClientSideQBSearchProps,
|
||||
): React.ReactElement {
|
||||
const {
|
||||
onChange,
|
||||
placeholder,
|
||||
className,
|
||||
suffixIcon,
|
||||
whereClauseConfig,
|
||||
attributeValuesMap,
|
||||
attributeKeys,
|
||||
filters,
|
||||
} = props;
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const selectRef = useRef<BaseSelectRef>(null);
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
// create the tags from the initial query here, this should only be computed on the first load as post that tags and query will be always in sync.
|
||||
const [tags, setTags] = useState<ITag[]>(filters.items as ITag[]);
|
||||
|
||||
// this will maintain the current state of in process filter item
|
||||
const [currentFilterItem, setCurrentFilterItem] = useState<ITag | undefined>();
|
||||
|
||||
const [currentState, setCurrentState] = useState<DropdownState>(
|
||||
DropdownState.ATTRIBUTE_KEY,
|
||||
);
|
||||
|
||||
// to maintain the current running state until the tokenization happens for the tag
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
|
||||
const [dropdownOptions, setDropdownOptions] = useState<Option[]>([]);
|
||||
|
||||
const attributeValues = useMemo(() => {
|
||||
if (currentFilterItem?.key?.key) {
|
||||
return attributeValuesMap?.[currentFilterItem.key.key];
|
||||
}
|
||||
return {
|
||||
stringAttributeValues: [],
|
||||
numberAttributeValues: [],
|
||||
boolAttributeValues: [],
|
||||
};
|
||||
}, [attributeValuesMap, currentFilterItem?.key?.key]);
|
||||
|
||||
const handleDropdownSelect = useCallback(
|
||||
(value: string) => {
|
||||
let parsedValue: BaseAutocompleteData | string;
|
||||
|
||||
try {
|
||||
parsedValue = JSON.parse(value);
|
||||
} catch {
|
||||
parsedValue = value;
|
||||
}
|
||||
if (currentState === DropdownState.ATTRIBUTE_KEY) {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
...prev,
|
||||
key: parsedValue as BaseAutocompleteData,
|
||||
op: '',
|
||||
value: '',
|
||||
}));
|
||||
setCurrentState(DropdownState.OPERATOR);
|
||||
setSearchValue((parsedValue as BaseAutocompleteData)?.key);
|
||||
} else if (currentState === DropdownState.OPERATOR) {
|
||||
if (value === OPERATORS.EXISTS || value === OPERATORS.NOT_EXISTS) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key,
|
||||
op: value,
|
||||
value: '',
|
||||
} as ITag,
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
key: prev?.key as BaseAutocompleteData,
|
||||
op: value as string,
|
||||
value: '',
|
||||
}));
|
||||
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
|
||||
setSearchValue(`${currentFilterItem?.key?.key} ${value}`);
|
||||
}
|
||||
} else if (currentState === DropdownState.ATTRIBUTE_VALUE) {
|
||||
const operatorType =
|
||||
operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID';
|
||||
const isMulti = operatorType === QUERY_BUILDER_SEARCH_VALUES.MULTIPLY;
|
||||
|
||||
if (isMulti) {
|
||||
const { tagKey, tagOperator, tagValue } = getTagToken(searchValue);
|
||||
// this condition takes care of adding the IN/NIN multi values when pressed enter on an already existing value.
|
||||
// not the best interaction but in sync with what we have today!
|
||||
if (tagValue.includes(String(value))) {
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
setCurrentFilterItem(undefined);
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key,
|
||||
op: currentFilterItem?.op,
|
||||
value: tagValue,
|
||||
} as ITag,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
// this is for adding subsequent comma seperated values
|
||||
const newSearch = [...tagValue];
|
||||
newSearch[newSearch.length === 0 ? 0 : newSearch.length - 1] = value;
|
||||
const newSearchValue = newSearch.join(',');
|
||||
setSearchValue(`${tagKey} ${tagOperator} ${newSearchValue},`);
|
||||
} else {
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
setCurrentFilterItem(undefined);
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key,
|
||||
op: currentFilterItem?.op,
|
||||
value,
|
||||
} as ITag,
|
||||
]);
|
||||
}
|
||||
}
|
||||
},
|
||||
[currentFilterItem?.key, currentFilterItem?.op, currentState, searchValue],
|
||||
);
|
||||
|
||||
const handleSearch = useCallback((value: string) => {
|
||||
setSearchValue(value);
|
||||
}, []);
|
||||
|
||||
const onInputKeyDownHandler = useCallback(
|
||||
(event: KeyboardEvent<Element>): void => {
|
||||
if (event.key === 'Backspace' && !searchValue) {
|
||||
event.stopPropagation();
|
||||
setTags((prev) => prev.slice(0, -1));
|
||||
}
|
||||
},
|
||||
[searchValue],
|
||||
);
|
||||
|
||||
const handleOnBlur = useCallback((): void => {
|
||||
if (searchValue) {
|
||||
const operatorType =
|
||||
operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID';
|
||||
// if key is added and operator is not present then convert to body CONTAINS key
|
||||
if (
|
||||
currentFilterItem?.key &&
|
||||
isEmpty(currentFilterItem?.op) &&
|
||||
whereClauseConfig?.customKey === 'body' &&
|
||||
whereClauseConfig?.customOp === OPERATORS.CONTAINS
|
||||
) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: {
|
||||
key: 'body',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
id: 'body--string----true',
|
||||
},
|
||||
op: OPERATORS.CONTAINS,
|
||||
value: currentFilterItem?.key?.key,
|
||||
},
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else if (
|
||||
currentFilterItem?.op === OPERATORS.EXISTS ||
|
||||
currentFilterItem?.op === OPERATORS.NOT_EXISTS
|
||||
) {
|
||||
// is exists and not exists operator is present then convert directly to tag! no need of value here
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key,
|
||||
op: currentFilterItem?.op,
|
||||
value: '',
|
||||
},
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else if (
|
||||
// if the current state is in sync with the kind of operator used then convert into a tag
|
||||
validationMapper[operatorType]?.(
|
||||
isArray(currentFilterItem?.value)
|
||||
? currentFilterItem?.value.length || 0
|
||||
: 1,
|
||||
)
|
||||
) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key as BaseAutocompleteData,
|
||||
op: currentFilterItem?.op as string,
|
||||
value: currentFilterItem?.value || '',
|
||||
},
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentFilterItem?.key,
|
||||
currentFilterItem?.op,
|
||||
currentFilterItem?.value,
|
||||
searchValue,
|
||||
whereClauseConfig?.customKey,
|
||||
whereClauseConfig?.customOp,
|
||||
]);
|
||||
|
||||
// this useEffect takes care of tokenisation based on the search state
|
||||
useEffect(() => {
|
||||
// if there is no search value reset to the default state
|
||||
if (!searchValue) {
|
||||
setCurrentFilterItem(undefined);
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
}
|
||||
|
||||
// split the current search value based on delimiters
|
||||
const { tagKey, tagOperator, tagValue } = getTagToken(searchValue);
|
||||
|
||||
if (
|
||||
// Case 1 - if key is defined but the search text doesn't match with the set key,
|
||||
// can happen when user selects from dropdown and then deletes a few characters
|
||||
currentFilterItem?.key &&
|
||||
currentFilterItem?.key?.key !== tagKey.split(' ')[0]
|
||||
) {
|
||||
setCurrentFilterItem(undefined);
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else if (tagOperator && isEmpty(currentFilterItem?.op)) {
|
||||
// Case 2 -> key is set and now typing for the operator
|
||||
if (
|
||||
tagOperator === OPERATORS.EXISTS ||
|
||||
tagOperator === OPERATORS.NOT_EXISTS
|
||||
) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key,
|
||||
op: tagOperator,
|
||||
value: '',
|
||||
} as ITag,
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
key: prev?.key as BaseAutocompleteData,
|
||||
op: tagOperator,
|
||||
value: '',
|
||||
}));
|
||||
|
||||
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
|
||||
}
|
||||
} else if (
|
||||
// Case 3 -> selected operator from dropdown and then erased a part of it
|
||||
!isEmpty(currentFilterItem?.op) &&
|
||||
tagOperator !== currentFilterItem?.op
|
||||
) {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
key: prev?.key as BaseAutocompleteData,
|
||||
op: '',
|
||||
value: '',
|
||||
}));
|
||||
setCurrentState(DropdownState.OPERATOR);
|
||||
} else if (currentState === DropdownState.ATTRIBUTE_VALUE) {
|
||||
// Case 4 -> the final value state where we set the current filter values and the tokenisation happens on either
|
||||
// dropdown click or blur event
|
||||
const currentValue = {
|
||||
key: currentFilterItem?.key as BaseAutocompleteData,
|
||||
op: currentFilterItem?.op as string,
|
||||
value: tagValue,
|
||||
};
|
||||
if (!isEqual(currentValue, currentFilterItem)) {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
key: prev?.key as BaseAutocompleteData,
|
||||
op: prev?.op as string,
|
||||
value: tagValue,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentFilterItem,
|
||||
currentFilterItem?.key,
|
||||
currentFilterItem?.op,
|
||||
searchValue,
|
||||
currentState,
|
||||
]);
|
||||
|
||||
// the useEffect takes care of setting the dropdown values correctly on change of the current state
|
||||
useEffect(() => {
|
||||
if (currentState === DropdownState.ATTRIBUTE_KEY) {
|
||||
const filteredAttributeKeys = attributeKeys.filter((key) =>
|
||||
key.key.startsWith(searchValue),
|
||||
);
|
||||
setDropdownOptions(
|
||||
filteredAttributeKeys?.map(
|
||||
(key) =>
|
||||
({
|
||||
label: key.key,
|
||||
value: key,
|
||||
}) as Option,
|
||||
) || [],
|
||||
);
|
||||
}
|
||||
if (currentState === DropdownState.OPERATOR) {
|
||||
const keyOperator = searchValue.split(' ');
|
||||
const partialOperator = keyOperator?.[1];
|
||||
const strippedKey = keyOperator?.[0];
|
||||
|
||||
let operatorOptions;
|
||||
if (currentFilterItem?.key?.dataType) {
|
||||
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES[
|
||||
currentFilterItem.key
|
||||
.dataType as keyof typeof QUERY_BUILDER_OPERATORS_BY_TYPES
|
||||
].map((operator) => ({
|
||||
label: operator,
|
||||
value: operator,
|
||||
}));
|
||||
|
||||
if (partialOperator) {
|
||||
operatorOptions = operatorOptions.filter((op) =>
|
||||
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
||||
);
|
||||
}
|
||||
setDropdownOptions(operatorOptions);
|
||||
} else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) {
|
||||
operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({
|
||||
label: operator,
|
||||
value: operator,
|
||||
}));
|
||||
setDropdownOptions(operatorOptions);
|
||||
} else {
|
||||
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map(
|
||||
(operator) => ({
|
||||
label: operator,
|
||||
value: operator,
|
||||
}),
|
||||
);
|
||||
|
||||
if (partialOperator) {
|
||||
operatorOptions = operatorOptions.filter((op) =>
|
||||
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
||||
);
|
||||
}
|
||||
setDropdownOptions(operatorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentState === DropdownState.ATTRIBUTE_VALUE) {
|
||||
const values: Array<string | number | boolean> = [];
|
||||
const { tagValue } = getTagToken(searchValue);
|
||||
if (isArray(tagValue)) {
|
||||
if (!isEmpty(tagValue[tagValue.length - 1])) {
|
||||
values.push(tagValue[tagValue.length - 1]);
|
||||
}
|
||||
} else if (!isEmpty(tagValue)) {
|
||||
values.push(tagValue);
|
||||
}
|
||||
|
||||
const currentAttributeValues =
|
||||
attributeValues?.stringAttributeValues ||
|
||||
attributeValues?.numberAttributeValues ||
|
||||
attributeValues?.boolAttributeValues ||
|
||||
[];
|
||||
|
||||
values.push(...currentAttributeValues);
|
||||
|
||||
if (attributeValuesMap) {
|
||||
setDropdownOptions(
|
||||
values.map(
|
||||
(val) =>
|
||||
({
|
||||
label: checkCommaInValue(String(val)),
|
||||
value: val,
|
||||
}) as Option,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// If attributeValuesMap is not provided, don't set dropdown options
|
||||
setDropdownOptions([]);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
attributeValues,
|
||||
currentFilterItem?.key?.dataType,
|
||||
currentState,
|
||||
attributeKeys,
|
||||
searchValue,
|
||||
attributeValuesMap,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const filterTags: IBuilderQuery['filters'] = {
|
||||
op: 'AND',
|
||||
items: [],
|
||||
};
|
||||
tags.forEach((tag) => {
|
||||
const computedTagValue =
|
||||
tag.value &&
|
||||
Array.isArray(tag.value) &&
|
||||
tag.value[tag.value.length - 1] === ''
|
||||
? tag.value?.slice(0, -1)
|
||||
: (tag.value ?? '');
|
||||
filterTags.items.push({
|
||||
id: tag.id || uuid().slice(0, 8),
|
||||
key: tag.key,
|
||||
op: getOperatorValue(tag.op),
|
||||
value: computedTagValue,
|
||||
});
|
||||
});
|
||||
|
||||
if (!isEqual(filters, filterTags)) {
|
||||
onChange(filterTags);
|
||||
setTags(
|
||||
filterTags.items.map((tag) => ({
|
||||
...tag,
|
||||
op: getOperatorFromValue(tag.op),
|
||||
})) as ITag[],
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tags]);
|
||||
|
||||
const queryTags = useMemo(
|
||||
() => tags.map((tag) => `${tag.key.key} ${tag.op} ${tag.value}`),
|
||||
[tags],
|
||||
);
|
||||
|
||||
const onTagRender = ({
|
||||
value,
|
||||
closable,
|
||||
onClose,
|
||||
}: CustomTagProps): React.ReactElement => {
|
||||
const { tagOperator } = getTagToken(value);
|
||||
const isInNin = isInNInOperator(tagOperator);
|
||||
const chipValue = isInNin
|
||||
? value?.trim()?.replace(/,\s*$/, '')
|
||||
: value?.trim();
|
||||
|
||||
const indexInQueryTags = queryTags.findIndex((qTag) => isEqual(qTag, value));
|
||||
const tagDetails = tags[indexInQueryTags];
|
||||
|
||||
const onCloseHandler = (): void => {
|
||||
onClose();
|
||||
setSearchValue('');
|
||||
setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails)));
|
||||
};
|
||||
|
||||
const tagEditHandler = (value: string): void => {
|
||||
setCurrentFilterItem(tagDetails);
|
||||
setSearchValue(value);
|
||||
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
|
||||
setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails)));
|
||||
};
|
||||
|
||||
const isDisabled = !!searchValue;
|
||||
|
||||
return (
|
||||
<span className="qb-search-bar-tokenised-tags">
|
||||
<Badge
|
||||
color="vanilla"
|
||||
className={tagDetails?.key?.type || ''}
|
||||
closable={!searchValue && closable}
|
||||
onClose={(e): void => {
|
||||
e.preventDefault();
|
||||
onCloseHandler();
|
||||
}}
|
||||
>
|
||||
<Tooltip title={chipValue}>
|
||||
<TypographyText
|
||||
$isInNin={isInNin}
|
||||
$isEnabled={!!searchValue}
|
||||
$disabled={isDisabled}
|
||||
onClick={(): void => {
|
||||
if (!isDisabled) {
|
||||
tagEditHandler(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{chipValue}
|
||||
</TypographyText>
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const suffixIconContent = useMemo(() => {
|
||||
if (suffixIcon) {
|
||||
return suffixIcon;
|
||||
}
|
||||
return isOpen ? (
|
||||
<ChevronUp
|
||||
size={14}
|
||||
color={isDarkMode ? Color.TEXT_VANILLA_100 : Color.TEXT_INK_100}
|
||||
/>
|
||||
) : (
|
||||
<ChevronDown
|
||||
size={14}
|
||||
color={isDarkMode ? Color.TEXT_VANILLA_100 : Color.TEXT_INK_100}
|
||||
/>
|
||||
);
|
||||
}, [isDarkMode, isOpen, suffixIcon]);
|
||||
|
||||
return (
|
||||
<div className="query-builder-search-v2 ">
|
||||
<Select
|
||||
ref={selectRef}
|
||||
getPopupContainer={popupContainer}
|
||||
virtual={false}
|
||||
showSearch
|
||||
tagRender={onTagRender}
|
||||
transitionName=""
|
||||
choiceTransitionName=""
|
||||
filterOption={false}
|
||||
open={isOpen}
|
||||
suffixIcon={suffixIconContent}
|
||||
onDropdownVisibleChange={setIsOpen}
|
||||
autoClearSearchValue={false}
|
||||
mode="multiple"
|
||||
placeholder={placeholder}
|
||||
value={queryTags}
|
||||
searchValue={searchValue}
|
||||
className={className}
|
||||
rootClassName="query-builder-search client-side-qb-search"
|
||||
disabled={!attributeKeys.length}
|
||||
style={selectStyle}
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleDropdownSelect}
|
||||
onInputKeyDown={onInputKeyDownHandler}
|
||||
notFoundContent={null}
|
||||
showAction={['focus']}
|
||||
onBlur={handleOnBlur}
|
||||
>
|
||||
{dropdownOptions.map((option) => {
|
||||
let val = option.value;
|
||||
try {
|
||||
if (isObject(option.value)) {
|
||||
val = JSON.stringify(option.value);
|
||||
} else {
|
||||
val = option.value;
|
||||
}
|
||||
} catch {
|
||||
val = option.value;
|
||||
}
|
||||
return (
|
||||
<Select.Option key={isObject(val) ? `select-option` : val} value={val}>
|
||||
<Suggestions
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
option={currentState}
|
||||
searchValue={searchValue}
|
||||
/>
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ClientSideQBSearch.defaultProps = {
|
||||
placeholder: PLACEHOLDER,
|
||||
className: '',
|
||||
suffixIcon: null,
|
||||
whereClauseConfig: {},
|
||||
attributeValuesMap: {},
|
||||
};
|
||||
|
||||
export default ClientSideQBSearch;
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from 'chart.js';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
|
||||
import { generateGridTitle } from 'utils/generateGridTitle';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
|
||||
@@ -19,6 +19,7 @@ export type LogDetailProps = {
|
||||
onScrollToLog?: (logId: string) => void;
|
||||
handleOpenInExplorer?: MouseEventHandler;
|
||||
getContainer?: DrawerProps['getContainer'];
|
||||
onApplyLogFilter?: (expression: string) => void;
|
||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
|
||||
Pick<DrawerProps, 'onClose'>;
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Link,
|
||||
} from '@signozhq/icons';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { normalizeTimeToMs } from 'utils/timeUtils';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { MouseEvent, MouseEventHandler } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
@@ -67,6 +68,11 @@ function LogDetailsHeader({
|
||||
},
|
||||
];
|
||||
|
||||
const rawTimestamp = log.date ?? log.timestamp;
|
||||
const displayTimestamp = Number.isNaN(Number(rawTimestamp))
|
||||
? rawTimestamp
|
||||
: normalizeTimeToMs(rawTimestamp);
|
||||
|
||||
return (
|
||||
<div className={styles.header} data-log-detail-ignore="true">
|
||||
<div className={styles.leftSection}>
|
||||
@@ -76,7 +82,7 @@ function LogDetailsHeader({
|
||||
data-testid="log-details-header-timestamp"
|
||||
>
|
||||
{formatTimezoneAdjustedTimestamp(
|
||||
log.date ?? log.timestamp,
|
||||
displayTimestamp,
|
||||
DATE_TIME_FORMATS.DASH_DATETIME,
|
||||
)}
|
||||
</Typography.Text>
|
||||
|
||||
@@ -91,6 +91,24 @@ describe('LogDetail drawer — header (isLogDetailsV2)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes a nanosecond-epoch timestamp in the header', () => {
|
||||
localStorage.setItem(LOCALSTORAGE.PREFERRED_TIMEZONE, 'UTC');
|
||||
|
||||
// Same instant as mockLog but as epoch nanoseconds (e.g. dashboard list panel).
|
||||
// Must scale to ms, not render a wildly wrong date.
|
||||
renderDrawer({
|
||||
log: {
|
||||
...mockLog,
|
||||
date: '1705311930000000000',
|
||||
timestamp: 1705311930000000000,
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('log-details-header-timestamp')).toHaveTextContent(
|
||||
'Jan 15, 2024 ⎯ 09:45:30',
|
||||
);
|
||||
});
|
||||
|
||||
it('copies the log link from the ⋯ menu', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ function LogDetailInner({
|
||||
onScrollToLog,
|
||||
handleOpenInExplorer,
|
||||
getContainer,
|
||||
onApplyLogFilter,
|
||||
}: LogDetailInnerProps): JSX.Element {
|
||||
const initialContextQuery = useInitialQuery(log);
|
||||
const [contextQuery, setContextQuery] = useState<Query | undefined>(
|
||||
@@ -519,6 +520,7 @@ function LogDetailInner({
|
||||
selectedOptions={options}
|
||||
listViewPanelSelectedFields={listViewPanelSelectedFields}
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
onApplyLogFilter={onApplyLogFilter}
|
||||
/>
|
||||
)}
|
||||
{!isLogDetailsV2 && selectedView === VIEW_TYPES.JSON && (
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
// v2 is rolled out only on the logs explorer route for now; every other surface
|
||||
// (dashboards, infra monitoring, etc.) keeps the v1 log details view.
|
||||
export function useIsLogDetailsV2(): boolean {
|
||||
const { pathname } = useLocation();
|
||||
return pathname === ROUTES.LOGS_EXPLORER;
|
||||
return (
|
||||
pathname === ROUTES.LOGS_EXPLORER ||
|
||||
pathname.startsWith(ROUTES.INFRASTRUCTURE_MONITORING_BASE) ||
|
||||
pathname.startsWith(`${ROUTES.ALL_DASHBOARD}/`)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,585 +0,0 @@
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import VariableItem from '../../../container/DashboardContainer/DashboardVariablesSelection/VariableItem';
|
||||
|
||||
// Mock the dashboard variables query
|
||||
jest.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
payload: {
|
||||
variableValues: ['option1', 'option2', 'option3', 'option4'],
|
||||
},
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock scrollIntoView which isn't available in JSDOM
|
||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
|
||||
// Constants
|
||||
const TEST_VARIABLE_NAME = 'test_variable';
|
||||
const TEST_VARIABLE_ID = 'test-var-id';
|
||||
|
||||
// Create a mock store
|
||||
const mockStore = configureStore([])({
|
||||
globalTime: {
|
||||
minTime: Date.now() - 3600000, // 1 hour ago
|
||||
maxTime: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
// Test data
|
||||
const createMockVariable = (
|
||||
overrides: Partial<IDashboardVariable> = {},
|
||||
): IDashboardVariable => ({
|
||||
id: TEST_VARIABLE_ID,
|
||||
name: TEST_VARIABLE_NAME,
|
||||
description: 'Test variable description',
|
||||
type: 'QUERY',
|
||||
queryValue: 'SELECT DISTINCT value FROM table',
|
||||
customValue: '',
|
||||
sort: 'ASC',
|
||||
multiSelect: false,
|
||||
showALLOption: true,
|
||||
selectedValue: [],
|
||||
allSelected: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
function TestWrapper({ children }: { children: React.ReactNode }): JSX.Element {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Provider store={mockStore}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<VirtuosoMockContext.Provider
|
||||
value={{ viewportHeight: 300, itemHeight: 40 }}
|
||||
>
|
||||
{children}
|
||||
</VirtuosoMockContext.Provider>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('VariableItem Integration Tests', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
let mockOnValueUpdate: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
user = userEvent.setup();
|
||||
mockOnValueUpdate = jest.fn();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
// ===== 1. INTEGRATION WITH CUSTOMSELECT =====
|
||||
describe('CustomSelect Integration (VI)', () => {
|
||||
it('VI-01: Single select variable integration', async () => {
|
||||
const variable = createMockVariable({
|
||||
multiSelect: false,
|
||||
type: 'CUSTOM',
|
||||
customValue: 'option1,option2,option3',
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<VariableItem
|
||||
variableData={variable}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
// Should render with CustomSelect
|
||||
const combobox = screen.getByRole('combobox');
|
||||
expect(combobox).toBeInTheDocument();
|
||||
|
||||
await user.click(combobox);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('option1')).toBeInTheDocument();
|
||||
expect(screen.getByText('option2')).toBeInTheDocument();
|
||||
expect(screen.getByText('option3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Select an option
|
||||
const option1 = screen.getByText('option1');
|
||||
await user.click(option1);
|
||||
|
||||
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
||||
TEST_VARIABLE_NAME,
|
||||
TEST_VARIABLE_ID,
|
||||
'option1',
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 2. INTEGRATION WITH CUSTOMMULTISELECT =====
|
||||
describe('CustomMultiSelect Integration (VI)', () => {
|
||||
it('VI-02: Multi select variable integration', async () => {
|
||||
const variable = createMockVariable({
|
||||
multiSelect: true,
|
||||
type: 'CUSTOM',
|
||||
customValue: 'option1,option2,option3,option4',
|
||||
showALLOption: true,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<VariableItem
|
||||
variableData={variable}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
// Should render with CustomMultiSelect
|
||||
const combobox = screen.getByRole('combobox');
|
||||
expect(combobox).toBeInTheDocument();
|
||||
|
||||
await user.click(combobox);
|
||||
|
||||
await waitFor(() => {
|
||||
// Should show ALL option
|
||||
expect(screen.getByText('ALL')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Wait for Virtuoso to render the custom options
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByText('option1')).toBeInTheDocument();
|
||||
expect(screen.getByText('option2')).toBeInTheDocument();
|
||||
expect(screen.getByText('option3')).toBeInTheDocument();
|
||||
expect(screen.getByText('option4')).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 3. TEXTBOX VARIABLE TYPE =====
|
||||
describe('Textbox Variable Integration', () => {
|
||||
it('VI-03: Textbox variable handling', async () => {
|
||||
const variable = createMockVariable({
|
||||
type: 'TEXTBOX',
|
||||
selectedValue: 'initial-value',
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<VariableItem
|
||||
variableData={variable}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
// Should render a regular input
|
||||
const textInput = screen.getByDisplayValue('initial-value');
|
||||
expect(textInput).toBeInTheDocument();
|
||||
expect(textInput.tagName).toBe('INPUT');
|
||||
|
||||
// Clear and type new value
|
||||
await user.clear(textInput);
|
||||
await user.type(textInput, 'new-text-value');
|
||||
|
||||
// Blur the input to trigger the value update
|
||||
await user.tab();
|
||||
|
||||
// Should call onValueUpdate after blur
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
||||
TEST_VARIABLE_NAME,
|
||||
TEST_VARIABLE_ID,
|
||||
'new-text-value',
|
||||
false,
|
||||
);
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 4. VALUE PERSISTENCE AND STATE MANAGEMENT =====
|
||||
describe('Value Persistence and State Management', () => {
|
||||
it('VI-04: All selected state handling', () => {
|
||||
const variable = createMockVariable({
|
||||
multiSelect: true,
|
||||
type: 'CUSTOM',
|
||||
customValue: 'service1,service2,service3',
|
||||
selectedValue: ['service1', 'service2', 'service3'],
|
||||
allSelected: true,
|
||||
showALLOption: true,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<VariableItem
|
||||
variableData={variable}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
// Should show "ALL" instead of individual values
|
||||
expect(screen.getByText('ALL')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('VI-05: Dropdown behavior with temporary selections', async () => {
|
||||
const variable = createMockVariable({
|
||||
multiSelect: true,
|
||||
type: 'CUSTOM',
|
||||
customValue: 'item1,item2,item3',
|
||||
selectedValue: ['item1'],
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<VariableItem
|
||||
variableData={variable}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
const combobox = screen.getByRole('combobox');
|
||||
await user.click(combobox);
|
||||
|
||||
// Wait for dropdown to open
|
||||
await waitFor(() => {
|
||||
const dropdown = document.querySelector('.ant-select-dropdown');
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify the component renders without crashing
|
||||
expect(combobox).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 6. ACCESSIBILITY AND USER EXPERIENCE =====
|
||||
describe('Accessibility and User Experience', () => {
|
||||
it('VI-06: Variable description tooltip', async () => {
|
||||
const variable = createMockVariable({
|
||||
description: 'This variable controls the service selection',
|
||||
type: 'CUSTOM',
|
||||
customValue: 'service1,service2',
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<VariableItem
|
||||
variableData={variable}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
// Should show info icon
|
||||
const infoIcon = document.querySelector('.info-icon');
|
||||
expect(infoIcon).toBeInTheDocument();
|
||||
|
||||
// Hover to show tooltip
|
||||
if (infoIcon) {
|
||||
await user.hover(infoIcon);
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('This variable controls the service selection'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('VI-07: Variable name display', () => {
|
||||
const variable = createMockVariable({
|
||||
name: 'service_name',
|
||||
type: 'CUSTOM',
|
||||
customValue: 'service1,service2',
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<VariableItem
|
||||
variableData={variable}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
// Should show variable name with $ prefix
|
||||
expect(screen.getByText('$service_name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('VI-08: Max tag count behavior', async () => {
|
||||
const variable = createMockVariable({
|
||||
multiSelect: true,
|
||||
type: 'CUSTOM',
|
||||
customValue: 'tag1,tag2,tag3,tag4,tag5,tag6,tag7',
|
||||
selectedValue: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'],
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<VariableItem
|
||||
variableData={variable}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
// Wait for component to render
|
||||
await waitFor(() => {
|
||||
const combobox = screen.getByRole('combobox');
|
||||
expect(combobox).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Should show limited number of tags with "+ X more"
|
||||
const tags = document.querySelectorAll('.ant-select-selection-item');
|
||||
|
||||
// The component should render without crashing
|
||||
expect(tags.length).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 8. SEARCH INTERACTION TESTS =====
|
||||
describe('Search Interaction Tests', () => {
|
||||
it('VI-14: Search persistence across dropdown open/close', async () => {
|
||||
const variable = createMockVariable({
|
||||
type: 'CUSTOM',
|
||||
customValue: 'option1,option2,option3',
|
||||
multiSelect: true,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<VariableItem
|
||||
variableData={variable}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
const combobox = screen.getByRole('combobox');
|
||||
await user.click(combobox);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('ALL')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const searchInput = document.querySelector(
|
||||
'.ant-select-selection-search-input',
|
||||
);
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
|
||||
if (searchInput) {
|
||||
await user.type(searchInput, 'search-text');
|
||||
}
|
||||
|
||||
// Verify search text is in input
|
||||
await waitFor(() => {
|
||||
expect(searchInput).toHaveValue('search-text');
|
||||
});
|
||||
|
||||
// Press Escape to close dropdown
|
||||
await user.keyboard('{Escape}');
|
||||
|
||||
// Dropdown should close and search text should be cleared
|
||||
await waitFor(() => {
|
||||
const dropdown = document.querySelector('.ant-select-dropdown');
|
||||
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
|
||||
expect(searchInput).toHaveValue('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 9. ADVANCED KEYBOARD NAVIGATION =====
|
||||
describe('Advanced Keyboard Navigation (VI)', () => {
|
||||
it('VI-15: Shift + Arrow + Del chip deletion in multiselect', async () => {
|
||||
const variable = createMockVariable({
|
||||
type: 'CUSTOM',
|
||||
customValue: 'option1,option2,option3',
|
||||
multiSelect: true,
|
||||
selectedValue: ['option1', 'option2', 'option3'],
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<VariableItem
|
||||
variableData={variable}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
const combobox = screen.getByRole('combobox');
|
||||
await user.click(combobox);
|
||||
|
||||
// Navigate to chips using arrow keys
|
||||
await user.keyboard('{ArrowLeft}');
|
||||
|
||||
// Use Shift + Arrow to navigate between chips
|
||||
await user.keyboard('{Shift>}{ArrowLeft}{/Shift}');
|
||||
|
||||
// Use Del to delete the active chip
|
||||
await user.keyboard('{Delete}');
|
||||
|
||||
// Note: The component may not immediately call onValueUpdate
|
||||
// This test verifies the chip deletion behavior
|
||||
await waitFor(() => {
|
||||
// Check if a chip was removed from the selection
|
||||
const selectionItems = document.querySelectorAll(
|
||||
'.ant-select-selection-item',
|
||||
);
|
||||
expect(selectionItems.length).toBeLessThan(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 11. ADVANCED UI STATES =====
|
||||
describe('Advanced UI States (VI)', () => {
|
||||
it('VI-19: No data with previous value selected in variable', async () => {
|
||||
const variable = createMockVariable({
|
||||
type: 'CUSTOM',
|
||||
customValue: '',
|
||||
multiSelect: true,
|
||||
selectedValue: ['previous-value'],
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<VariableItem
|
||||
variableData={variable}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
// Wait for component to initialize
|
||||
await waitFor(() => {
|
||||
const combobox = screen.getByRole('combobox');
|
||||
expect(combobox).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const combobox = screen.getByRole('combobox');
|
||||
await user.click(combobox);
|
||||
|
||||
// Should show no data message (the component may not show this exact text)
|
||||
await waitFor(() => {
|
||||
// Check if dropdown is empty or shows no data indication
|
||||
const dropdown = document.querySelector('.ant-select-dropdown');
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify the component renders without crashing
|
||||
expect(combobox).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('VI-20: Always editable accessibility in variable', async () => {
|
||||
const variable = createMockVariable({
|
||||
type: 'CUSTOM',
|
||||
customValue: 'option1,option2',
|
||||
multiSelect: true,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<VariableItem
|
||||
variableData={variable}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
const combobox = screen.getByRole('combobox');
|
||||
|
||||
// Should be editable
|
||||
expect(combobox).not.toBeDisabled();
|
||||
await user.click(combobox);
|
||||
expect(combobox).toHaveFocus();
|
||||
|
||||
// Should still be interactive
|
||||
expect(combobox).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 13. DROPDOWN PERSISTENCE =====
|
||||
describe('Dropdown Persistence (VI)', () => {
|
||||
it('VI-24: Dropdown stays open for non-save actions in variable', async () => {
|
||||
const variable = createMockVariable({
|
||||
type: 'CUSTOM',
|
||||
customValue: 'option1,option2,option3',
|
||||
multiSelect: true,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<VariableItem
|
||||
variableData={variable}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
// Wait for component to initialize
|
||||
await waitFor(() => {
|
||||
const combobox = screen.getByRole('combobox');
|
||||
expect(combobox).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const combobox = screen.getByRole('combobox');
|
||||
await user.click(combobox);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('ALL')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Navigate with arrow keys (non-save action)
|
||||
await user.keyboard('{ArrowDown}');
|
||||
await user.keyboard('{ArrowDown}');
|
||||
|
||||
// Dropdown should still be open
|
||||
await waitFor(() => {
|
||||
const dropdown = document.querySelector('.ant-select-dropdown');
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify the component renders without crashing
|
||||
expect(combobox).toBeInTheDocument();
|
||||
|
||||
// Only ESC should close the dropdown
|
||||
await user.keyboard('{Escape}');
|
||||
|
||||
await waitFor(() => {
|
||||
const dropdown = document.querySelector('.ant-select-dropdown');
|
||||
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,26 @@
|
||||
import { uniqueOptions } from 'container/DashboardContainer/DashboardVariablesSelection/util';
|
||||
|
||||
import { OptionData } from './types';
|
||||
|
||||
export const SPACEKEY = ' ';
|
||||
|
||||
export const ALL_SELECTED_VALUE = '__ALL__'; // Constant for the special value
|
||||
|
||||
/** De-duplicate options by `value`, keeping the first occurrence. */
|
||||
export const uniqueOptions = (options: OptionData[]): OptionData[] => {
|
||||
const deduped: OptionData[] = [];
|
||||
const seenValues = new Set<string>();
|
||||
|
||||
options.forEach((option) => {
|
||||
const value = option.value || '';
|
||||
if (seenValues.has(value)) {
|
||||
return;
|
||||
}
|
||||
seenValues.add(value);
|
||||
deduped.push(option);
|
||||
});
|
||||
|
||||
return deduped;
|
||||
};
|
||||
|
||||
export const prioritizeOrAddOptionForSingleSelect = (
|
||||
options: OptionData[],
|
||||
value: string,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Spline } from '@signozhq/icons';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import QueryTypeTag from '../QueryTypeTag';
|
||||
import QueryTypeTag from 'components/QueryTypeTag/QueryTypeTag';
|
||||
|
||||
interface IPlotTagProps {
|
||||
queryType: EQueryType;
|
||||
@@ -17,12 +17,6 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { dashboardData: undefined } => ({
|
||||
dashboardData: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Shrink the suggestion-fetch debounce (300ms in prod) so these integration
|
||||
// tests aren't paced by it; coalescing semantics stay intact.
|
||||
jest.mock('../QuerySearch/constants', () => ({
|
||||
|
||||
@@ -21,12 +21,6 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { dashboardData: undefined } => ({
|
||||
dashboardData: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
|
||||
const handleRunQuery = jest.fn();
|
||||
return {
|
||||
@@ -152,15 +146,16 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Wait for debounced API call (300ms debounce + some buffer)
|
||||
await waitFor(() => expect(mockedGetKeysOnMount).toHaveBeenCalled(), {
|
||||
timeout: 2000,
|
||||
});
|
||||
|
||||
const lastArgs = mockedGetKeysOnMount.mock.calls[
|
||||
mockedGetKeysOnMount.mock.calls.length - 1
|
||||
]?.[0] as { signal: unknown; searchText: string };
|
||||
expect(lastArgs).toMatchObject({ signal: DataSource.LOGS, searchText: '' });
|
||||
// Wait for the mount fetch specifically. A debounced fetch from an earlier test
|
||||
// can still land after mockClear(), so waiting on "any call" would let this
|
||||
// assert against that one instead and make the result order-dependent.
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(mockedGetKeysOnMount).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ signal: DataSource.LOGS, searchText: '' }),
|
||||
),
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('calls provided onRun on Mod-Enter', async () => {
|
||||
|
||||
@@ -31,12 +31,6 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { dashboardData: undefined } => ({
|
||||
dashboardData: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
|
||||
getKeySuggestions: jest.fn().mockResolvedValue({
|
||||
data: { data: { keys: {} } },
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
OPERATORS,
|
||||
QUERY_BUILDER_FUNCTIONS,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import FilterQueryLexer from 'parser/FilterQueryLexer';
|
||||
import FilterQueryParser, {
|
||||
AndExpressionContext,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { cloneDeep, isArray } from 'lodash-es';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import {
|
||||
useRecoverFromEmptyPage,
|
||||
UseRecoverFromEmptyPageParams,
|
||||
} from '../useRecoverFromEmptyPage';
|
||||
|
||||
const REPLACE = { history: 'replace' };
|
||||
|
||||
function renderRecovery(
|
||||
overrides: Partial<UseRecoverFromEmptyPageParams> = {},
|
||||
): { setPage: jest.Mock; rerender: (next?: unknown) => void } {
|
||||
const setPage = jest.fn();
|
||||
const props: UseRecoverFromEmptyPageParams = {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
rowCount: 10,
|
||||
total: 100,
|
||||
isFetching: false,
|
||||
setPage,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
const { rerender } = renderHook(
|
||||
(next: UseRecoverFromEmptyPageParams) => useRecoverFromEmptyPage(next),
|
||||
{ initialProps: props },
|
||||
);
|
||||
|
||||
return {
|
||||
setPage,
|
||||
rerender: (next?: unknown): void =>
|
||||
rerender({ ...props, ...(next as Partial<UseRecoverFromEmptyPageParams>) }),
|
||||
};
|
||||
}
|
||||
|
||||
describe('useRecoverFromEmptyPage', () => {
|
||||
it('leaves the page alone while it still holds rows', () => {
|
||||
const { setPage } = renderRecovery({ page: 3, rowCount: 10 });
|
||||
|
||||
expect(setPage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('leaves the page alone on page 1 with no rows at all', () => {
|
||||
const { setPage } = renderRecovery({ page: 1, rowCount: 0, total: 0 });
|
||||
|
||||
expect(setPage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('jumps to the last page that holds data when the page is out of range', () => {
|
||||
const { setPage } = renderRecovery({
|
||||
page: 7,
|
||||
pageSize: 10,
|
||||
rowCount: 0,
|
||||
total: 25,
|
||||
});
|
||||
|
||||
expect(setPage).toHaveBeenCalledWith(3, REPLACE);
|
||||
});
|
||||
|
||||
it('replaces the history entry so the back button does not return to the empty page', () => {
|
||||
const { setPage } = renderRecovery({ page: 4, rowCount: 0, total: 10 });
|
||||
|
||||
expect(setPage).toHaveBeenCalledWith(1, REPLACE);
|
||||
});
|
||||
|
||||
it('falls back to page 1 when the total is unknown', () => {
|
||||
const { setPage } = renderRecovery({ page: 5, rowCount: 0, total: 0 });
|
||||
|
||||
expect(setPage).toHaveBeenCalledWith(1, REPLACE);
|
||||
});
|
||||
|
||||
it('steps back one page when the total claims the page should have data', () => {
|
||||
// total says 100 rows exist, yet page 5 came back empty — step back rather
|
||||
// than stall on a page the query cannot actually serve.
|
||||
const { setPage } = renderRecovery({
|
||||
page: 5,
|
||||
pageSize: 10,
|
||||
rowCount: 0,
|
||||
total: 100,
|
||||
});
|
||||
|
||||
expect(setPage).toHaveBeenCalledWith(4, REPLACE);
|
||||
});
|
||||
|
||||
it('clamps a page below the first one', () => {
|
||||
const { setPage } = renderRecovery({ page: 0, rowCount: 10 });
|
||||
|
||||
expect(setPage).toHaveBeenCalledWith(1, REPLACE);
|
||||
});
|
||||
|
||||
it('falls back to page 1 when pageSize is zero', () => {
|
||||
const { setPage } = renderRecovery({
|
||||
page: 5,
|
||||
pageSize: 0,
|
||||
rowCount: 0,
|
||||
total: 100,
|
||||
});
|
||||
|
||||
expect(setPage).toHaveBeenCalledWith(1, REPLACE);
|
||||
});
|
||||
|
||||
it('waits for the request to settle before moving the user', () => {
|
||||
const { setPage, rerender } = renderRecovery({
|
||||
page: 3,
|
||||
rowCount: 0,
|
||||
total: 10,
|
||||
isFetching: true,
|
||||
});
|
||||
|
||||
expect(setPage).not.toHaveBeenCalled();
|
||||
|
||||
rerender({ page: 3, rowCount: 0, total: 10, isFetching: false });
|
||||
|
||||
expect(setPage).toHaveBeenCalledWith(1, REPLACE);
|
||||
});
|
||||
|
||||
it('clamps a page below the first one even when the query failed', () => {
|
||||
// A negative offset is what made the request fail (400 "offset cannot be
|
||||
// negative"), so retrying the same page loops forever — clamp regardless.
|
||||
const { setPage } = renderRecovery({
|
||||
page: 0,
|
||||
rowCount: 0,
|
||||
total: 0,
|
||||
isDisabled: true,
|
||||
});
|
||||
|
||||
expect(setPage).toHaveBeenCalledWith(1, REPLACE);
|
||||
});
|
||||
|
||||
it('clamps a page below the first one while the query is still in flight', () => {
|
||||
const { setPage } = renderRecovery({
|
||||
page: -2,
|
||||
rowCount: 0,
|
||||
isFetching: true,
|
||||
});
|
||||
|
||||
expect(setPage).toHaveBeenCalledWith(1, REPLACE);
|
||||
});
|
||||
|
||||
it('keeps the page when the query failed so a retry lands where the user was', () => {
|
||||
const { setPage } = renderRecovery({
|
||||
page: 3,
|
||||
rowCount: 0,
|
||||
total: 0,
|
||||
isDisabled: true,
|
||||
});
|
||||
|
||||
expect(setPage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clamps a page below the first one exactly once while the request settles', () => {
|
||||
// The clamp runs ahead of both gates, so a request settling underneath an
|
||||
// uncorrected page must not re-issue the same history rewrite.
|
||||
const { setPage, rerender } = renderRecovery({
|
||||
page: 0,
|
||||
rowCount: 0,
|
||||
total: 0,
|
||||
isFetching: true,
|
||||
});
|
||||
|
||||
expect(setPage).toHaveBeenCalledTimes(1);
|
||||
|
||||
rerender({ page: 0, rowCount: 0, total: 0, isFetching: false });
|
||||
|
||||
expect(setPage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('stops correcting once the corrected page comes back with rows', () => {
|
||||
const { setPage, rerender } = renderRecovery({
|
||||
page: 7,
|
||||
pageSize: 10,
|
||||
rowCount: 0,
|
||||
total: 25,
|
||||
});
|
||||
|
||||
expect(setPage).toHaveBeenCalledWith(3, REPLACE);
|
||||
|
||||
// The correction lands: the query refetches, then resolves with the rows page 3 holds.
|
||||
rerender({ page: 3, pageSize: 10, rowCount: 0, total: 25, isFetching: true });
|
||||
rerender({
|
||||
page: 3,
|
||||
pageSize: 10,
|
||||
rowCount: 5,
|
||||
total: 25,
|
||||
isFetching: false,
|
||||
});
|
||||
|
||||
expect(setPage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not correct again while the same page is still being observed', () => {
|
||||
const { setPage, rerender } = renderRecovery({
|
||||
page: 5,
|
||||
pageSize: 10,
|
||||
rowCount: 0,
|
||||
total: 100,
|
||||
});
|
||||
|
||||
expect(setPage).toHaveBeenCalledTimes(1);
|
||||
|
||||
// A refetch cycle that leaves the page untouched — the correction is already in flight.
|
||||
rerender({
|
||||
page: 5,
|
||||
pageSize: 10,
|
||||
rowCount: 0,
|
||||
total: 100,
|
||||
isFetching: true,
|
||||
});
|
||||
rerender({
|
||||
page: 5,
|
||||
pageSize: 10,
|
||||
rowCount: 0,
|
||||
total: 100,
|
||||
isFetching: false,
|
||||
});
|
||||
|
||||
expect(setPage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('gives up on step-backs and jumps to page 1 when the total keeps lying', () => {
|
||||
// `total` claims 400 rows exist, but every page comes back empty. Walking back one
|
||||
// page at a time would cost a request per hop, so bail out to page 1 instead.
|
||||
const { setPage, rerender } = renderRecovery({
|
||||
page: 40,
|
||||
pageSize: 10,
|
||||
rowCount: 0,
|
||||
total: 400,
|
||||
});
|
||||
|
||||
expect(setPage).toHaveBeenNthCalledWith(1, 39, REPLACE);
|
||||
|
||||
rerender({ page: 39, pageSize: 10, rowCount: 0, total: 400 });
|
||||
|
||||
expect(setPage).toHaveBeenNthCalledWith(2, 38, REPLACE);
|
||||
|
||||
rerender({ page: 38, pageSize: 10, rowCount: 0, total: 400 });
|
||||
|
||||
expect(setPage).toHaveBeenNthCalledWith(3, 1, REPLACE);
|
||||
expect(setPage).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('corrects again when the user returns to a page that is still empty', () => {
|
||||
const { setPage, rerender } = renderRecovery({
|
||||
page: 3,
|
||||
pageSize: 10,
|
||||
rowCount: 0,
|
||||
total: 10,
|
||||
});
|
||||
|
||||
expect(setPage).toHaveBeenNthCalledWith(1, 1, REPLACE);
|
||||
|
||||
rerender({ page: 1, pageSize: 10, rowCount: 10, total: 10 });
|
||||
rerender({ page: 3, pageSize: 10, rowCount: 0, total: 10 });
|
||||
|
||||
expect(setPage).toHaveBeenNthCalledWith(2, 1, REPLACE);
|
||||
});
|
||||
|
||||
it('does not re-run the correction when setPage is a fresh function each render', () => {
|
||||
// The hook reads setPage through a ref, so an inline arrow must not turn the
|
||||
// ungated `page < 1` clamp into a per-render history rewrite.
|
||||
const setPage = jest.fn();
|
||||
const { rerender } = renderHook(
|
||||
() =>
|
||||
useRecoverFromEmptyPage({
|
||||
page: 0,
|
||||
pageSize: 10,
|
||||
rowCount: 0,
|
||||
total: 0,
|
||||
isFetching: false,
|
||||
setPage: (nextPage, options): void => setPage(nextPage, options),
|
||||
}),
|
||||
{ initialProps: undefined },
|
||||
);
|
||||
|
||||
rerender(undefined);
|
||||
rerender(undefined);
|
||||
|
||||
expect(setPage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import {
|
||||
useStableTotalCount,
|
||||
UseStableTotalCountParams,
|
||||
} from '../useStableTotalCount';
|
||||
|
||||
function renderStableTotalCount(initial: UseStableTotalCountParams): {
|
||||
result: { current: number };
|
||||
rerender: (next: UseStableTotalCountParams) => void;
|
||||
} {
|
||||
const { result, rerender } = renderHook(
|
||||
(params: UseStableTotalCountParams) => useStableTotalCount(params),
|
||||
{ initialProps: initial },
|
||||
);
|
||||
|
||||
return { result, rerender };
|
||||
}
|
||||
|
||||
describe('useStableTotalCount', () => {
|
||||
it('reports the total once the load settles', () => {
|
||||
const { result } = renderStableTotalCount({
|
||||
total: 100,
|
||||
isLoading: false,
|
||||
resetKey: 'pods',
|
||||
});
|
||||
|
||||
expect(result.current).toBe(100);
|
||||
});
|
||||
|
||||
it('keeps the last count while the same list refetches', () => {
|
||||
const { result, rerender } = renderStableTotalCount({
|
||||
total: 100,
|
||||
isLoading: false,
|
||||
resetKey: 'pods',
|
||||
});
|
||||
|
||||
rerender({ total: 0, isLoading: true, resetKey: 'pods' });
|
||||
|
||||
expect(result.current).toBe(100);
|
||||
});
|
||||
|
||||
it('forgets the count when the list changes', () => {
|
||||
const { result, rerender } = renderStableTotalCount({
|
||||
total: 100,
|
||||
isLoading: false,
|
||||
resetKey: 'pods',
|
||||
});
|
||||
|
||||
rerender({ total: 0, isLoading: true, resetKey: 'nodes' });
|
||||
|
||||
expect(result.current).toBe(0);
|
||||
});
|
||||
|
||||
it('reports the new list total once it arrives', () => {
|
||||
const { result, rerender } = renderStableTotalCount({
|
||||
total: 100,
|
||||
isLoading: false,
|
||||
resetKey: 'pods',
|
||||
});
|
||||
|
||||
rerender({ total: 0, isLoading: true, resetKey: 'nodes' });
|
||||
rerender({ total: 25, isLoading: false, resetKey: 'nodes' });
|
||||
|
||||
expect(result.current).toBe(25);
|
||||
});
|
||||
|
||||
it('reports zero for a list that is genuinely empty', () => {
|
||||
const { result } = renderStableTotalCount({
|
||||
total: 0,
|
||||
isLoading: false,
|
||||
resetKey: 'pods',
|
||||
});
|
||||
|
||||
expect(result.current).toBe(0);
|
||||
});
|
||||
|
||||
it('holds nothing back when no reset key is given', () => {
|
||||
const { result, rerender } = renderStableTotalCount({
|
||||
total: 100,
|
||||
isLoading: false,
|
||||
resetKey: undefined,
|
||||
});
|
||||
|
||||
// Without a key there is no context to compare, so the cache behaves as it
|
||||
// always did: it survives the refetch.
|
||||
rerender({ total: 0, isLoading: true, resetKey: undefined });
|
||||
|
||||
expect(result.current).toBe(100);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ export * from './useCalculatedPageSize';
|
||||
export * from './useColumnState';
|
||||
export * from './useColumnStore';
|
||||
export * from './usePreferredPageSize.store';
|
||||
export * from './useRecoverFromEmptyPage';
|
||||
export * from './useTableParams';
|
||||
|
||||
/**
|
||||
@@ -285,6 +286,51 @@ export * from './useTableParams';
|
||||
*
|
||||
* **Pagination shows "Auto" option** when `calculatedPageSize` is passed, allowing users
|
||||
* to reset to auto-calculated size.
|
||||
*
|
||||
* **`setPage` accepts history options**: `setPage(page, { history: 'replace' })` rewrites the
|
||||
* current history entry instead of pushing a new one. Use `replace` for corrections the user
|
||||
* did not ask for — otherwise the back button walks straight back into the state that was just
|
||||
* corrected. Only applies when the page is synced to the URL; local (non-URL) pages ignore it.
|
||||
*
|
||||
* @example useRecoverFromEmptyPage — send the user back to a page that has data
|
||||
*
|
||||
* When rows disappear underneath the current page (filters narrowed, time range moved, items
|
||||
* deleted), the user is stranded on an empty page they cannot leave by scrolling. This hook
|
||||
* watches the fetched result and corrects the page with `history: 'replace'`, so the back
|
||||
* button does not return to the empty page.
|
||||
*
|
||||
* Correction rules:
|
||||
* - `page < 1` → jump to page 1, even while fetching or disabled. Such a page usually maps to a
|
||||
* negative offset the API rejects (400 `offset cannot be negative`), so the response can never
|
||||
* confirm the page is empty — deferring to it would strand the user on a permanent error.
|
||||
* - Page is empty and not page 1 → go to `min(ceil(total / pageSize), page - 1)`. When `total`
|
||||
* is trustworthy that lands on the last page holding data; when `total` is unknown or zero it
|
||||
* lands on page 1; and when `total` claims this page should have had rows it steps back a
|
||||
* single page. Repeated step-backs give up and jump to page 1 after the second one, so a
|
||||
* badly inflated `total` cannot walk the user down one request at a time.
|
||||
* - Page has rows, or the user is already on page 1 → do nothing (an empty page 1 means there
|
||||
* is genuinely nothing to show).
|
||||
*
|
||||
* Pass `isFetching` so the hook waits for the request to settle, and `isDisabled` so a failed
|
||||
* request is not mistaken for an empty page. Neither gate suppresses the `page < 1` clamp.
|
||||
*
|
||||
* ```tsx
|
||||
* import { useRecoverFromEmptyPage, useTableParams } from 'components/TanStackTableView';
|
||||
*
|
||||
* const { page, limit, setPage } = useTableParams(QUERY_PARAMS, { page: 1, limit: 20 });
|
||||
* const { data, isLoading, isFetching, isError } = useListQuery({ page, limit });
|
||||
*
|
||||
* useRecoverFromEmptyPage({
|
||||
* page,
|
||||
* pageSize: limit,
|
||||
* rowCount: data?.rows.length ?? 0,
|
||||
* total: data?.total ?? 0,
|
||||
* isFetching: isLoading || isFetching,
|
||||
* // Skip correction on errors — no rows there means "request failed", not "page is empty".
|
||||
* isDisabled: isError,
|
||||
* setPage,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
const TanStackTable = Object.assign(TanStackTableBase, {
|
||||
Text: TanStackTableText,
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { SetPageOptions } from './useTableParams';
|
||||
|
||||
const FIRST_PAGE = 1;
|
||||
const REPLACE_HISTORY: SetPageOptions = { history: 'replace' };
|
||||
|
||||
/**
|
||||
* How many single-page step-backs to attempt before giving up and going to page 1.
|
||||
*
|
||||
* A step-back only happens when `total` claims the current page should hold data but the
|
||||
* response came back empty. Each hop costs a request, so an inflated `total` on a high page
|
||||
* number would otherwise walk the user down one page at a time behind a spinner.
|
||||
*/
|
||||
const MAX_STEP_BACKS = 2;
|
||||
|
||||
type Correction = {
|
||||
from: number;
|
||||
to: number;
|
||||
};
|
||||
|
||||
export type UseRecoverFromEmptyPageParams = {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
rowCount: number;
|
||||
total: number;
|
||||
isFetching: boolean;
|
||||
isDisabled?: boolean;
|
||||
setPage: (page: number, options?: SetPageOptions) => void;
|
||||
};
|
||||
|
||||
export function useRecoverFromEmptyPage({
|
||||
page,
|
||||
pageSize,
|
||||
rowCount,
|
||||
total,
|
||||
isFetching,
|
||||
isDisabled = false,
|
||||
setPage,
|
||||
}: UseRecoverFromEmptyPageParams): void {
|
||||
const setPageRef = useRef(setPage);
|
||||
const lastCorrectionRef = useRef<Correction | null>(null);
|
||||
const stepBacksRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
setPageRef.current = setPage;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (lastCorrectionRef.current && lastCorrectionRef.current.from !== page) {
|
||||
lastCorrectionRef.current = null;
|
||||
}
|
||||
|
||||
const correctTo = (nextPage: number): boolean => {
|
||||
if (lastCorrectionRef.current?.to === nextPage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
lastCorrectionRef.current = { from: page, to: nextPage };
|
||||
setPageRef.current(nextPage, REPLACE_HISTORY);
|
||||
return true;
|
||||
};
|
||||
|
||||
// A page below the first one is invalid on its own terms — it usually maps to a
|
||||
// negative offset the API rejects outright, so waiting for a response that will
|
||||
// never arrive (or trusting a failed one) would strand the user for good.
|
||||
if (page < FIRST_PAGE) {
|
||||
stepBacksRef.current = 0;
|
||||
void correctTo(FIRST_PAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFetching || isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The page has data, or there is genuinely nothing to show anywhere.
|
||||
if (rowCount > 0 || page === FIRST_PAGE) {
|
||||
stepBacksRef.current = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPage = Math.floor(page);
|
||||
const lastPageWithData =
|
||||
pageSize > 0 && total > 0 ? Math.ceil(total / pageSize) : FIRST_PAGE;
|
||||
const nextPage = Math.max(
|
||||
FIRST_PAGE,
|
||||
Math.min(lastPageWithData, currentPage - 1),
|
||||
);
|
||||
|
||||
// `total` disagrees with the response: it says this page should have rows, so the
|
||||
// only safe move is one page back. Cap how often that repeats — every hop is a
|
||||
// request, and a badly inflated `total` would otherwise crawl down from page 40.
|
||||
const isStepBack = nextPage === currentPage - 1;
|
||||
|
||||
if (isStepBack && stepBacksRef.current >= MAX_STEP_BACKS) {
|
||||
if (correctTo(FIRST_PAGE)) {
|
||||
stepBacksRef.current = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (correctTo(nextPage) && isStepBack) {
|
||||
stepBacksRef.current += 1;
|
||||
}
|
||||
}, [isFetching, isDisabled, page, pageSize, rowCount, total]);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
export type UseStableTotalCountParams = {
|
||||
total: number | undefined;
|
||||
isLoading: boolean;
|
||||
/**
|
||||
* Identifies the list being counted. When it changes, the cached count is
|
||||
* dropped so the previous list's page count cannot outlive it.
|
||||
*/
|
||||
resetKey: string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Holds on to the last non-zero total so the pagination does not flash while the
|
||||
* same list refetches, and forgets it as soon as `resetKey` moves to another list.
|
||||
*/
|
||||
export function useStableTotalCount({
|
||||
total,
|
||||
isLoading,
|
||||
resetKey,
|
||||
}: UseStableTotalCountParams): number {
|
||||
const prevTotalRef = useRef(total || 0);
|
||||
const prevResetKeyRef = useRef(resetKey);
|
||||
|
||||
if (prevResetKeyRef.current !== resetKey) {
|
||||
prevResetKeyRef.current = resetKey;
|
||||
prevTotalRef.current = 0;
|
||||
}
|
||||
|
||||
if (total && total > 0) {
|
||||
prevTotalRef.current = total;
|
||||
}
|
||||
|
||||
return isLoading ? prevTotalRef.current : total || 0;
|
||||
}
|
||||
@@ -29,12 +29,16 @@ type Defaults = {
|
||||
cleanupOnUnmount?: boolean;
|
||||
};
|
||||
|
||||
export type SetPageOptions = {
|
||||
history?: 'push' | 'replace';
|
||||
};
|
||||
|
||||
export type TableParamsResult = {
|
||||
page: number;
|
||||
limit: number;
|
||||
orderBy: SortState | null;
|
||||
expanded: ExpandedState;
|
||||
setPage: (p: number) => void;
|
||||
setPage: (p: number, options?: SetPageOptions) => void;
|
||||
setLimit: (l: number) => void;
|
||||
setOrderBy: (s: SortState | null) => void;
|
||||
setExpanded: (updaterOrValue: Updater<ExpandedState>) => void;
|
||||
@@ -249,6 +253,17 @@ export function useTableParams(
|
||||
[],
|
||||
);
|
||||
|
||||
const setUrlPageWithOptions = useCallback(
|
||||
(page: number, options?: SetPageOptions): void => {
|
||||
void setUrlPage(page, options);
|
||||
},
|
||||
[setUrlPage],
|
||||
);
|
||||
|
||||
const setLocalPageValue = useCallback((page: number): void => {
|
||||
setLocalPage(page);
|
||||
}, []);
|
||||
|
||||
const orderByUrlMemoKey = `${urlOrderBy?.columnName}${urlOrderBy?.order}`;
|
||||
const prevOrderByRef = useRef<string | null>(null);
|
||||
|
||||
@@ -303,7 +318,7 @@ export function useTableParams(
|
||||
limit: useUrlForLimit ? urlLimit : localLimit,
|
||||
orderBy: (useUrlForOrderBy ? urlOrderBy : localOrderBy) as SortState | null,
|
||||
expanded: useUrlForExpanded ? urlExpanded : localExpanded,
|
||||
setPage: useUrlForPage ? setUrlPage : setLocalPage,
|
||||
setPage: useUrlForPage ? setUrlPageWithOptions : setLocalPageValue,
|
||||
setLimit: useUrlForLimit ? setUrlLimit : setLocalLimitWithPersist,
|
||||
setOrderBy: useUrlForOrderBy ? setUrlOrderBy : setLocalOrderBy,
|
||||
setExpanded: useUrlForExpanded ? setUrlExpanded : handleSetLocalExpanded,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { timeItems } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { timeItems } from 'constants/timePreference';
|
||||
|
||||
export const menuItems = timeItems.map((item) => ({
|
||||
key: item.enum,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import TimeItems, {
|
||||
timePreferance,
|
||||
timePreferenceType,
|
||||
} from 'container/NewWidget/RightContainer/timeItems';
|
||||
} from 'constants/timePreference';
|
||||
|
||||
import { menuItems } from './config';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { CircleAlert } from '@signozhq/icons';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { ThresholdProps } from 'types/api/widgets/threshold';
|
||||
|
||||
import { getBackgroundColorAndThresholdCheck } from './utils';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { evaluateThresholdWithConvertedValue } from 'container/GridTableComponent/utils';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { evaluateThresholdWithConvertedValue } from 'container/WidgetCard/TablePanel/utils';
|
||||
import { ThresholdProps } from 'types/api/widgets/threshold';
|
||||
|
||||
function doesValueSatisfyThreshold(
|
||||
rawValue: number,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Uplot from 'components/Uplot';
|
||||
import GridTableComponent from 'container/GridTableComponent';
|
||||
import GridValueComponent from 'container/GridValueComponent';
|
||||
import GridTableComponent from 'container/WidgetCard/TablePanel';
|
||||
import GridValueComponent from 'container/WidgetCard/ValuePanel';
|
||||
import LogsPanelComponent from 'container/LogsPanelTable/LogsPanelComponent';
|
||||
import TracesTableComponent from 'container/TracesTableComponent/TracesTableComponent';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -16,7 +16,6 @@ const ROUTES = {
|
||||
APPLICATION: '/services',
|
||||
ALL_DASHBOARD: '/dashboard',
|
||||
DASHBOARD: '/dashboard/:dashboardId',
|
||||
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
|
||||
DASHBOARD_PANEL_EDITOR: '/dashboard/:dashboardId/panel/:panelId',
|
||||
EDIT_ALERTS: '/alerts/edit',
|
||||
LIST_ALL_ALERT: '/alerts',
|
||||
|
||||
@@ -10,7 +10,6 @@ const fieldContextToSuggestionMap: Record<
|
||||
[TelemetrytypesFieldContextDTO.attribute]: 'attribute',
|
||||
// no maps for the following values on suggestion context
|
||||
[TelemetrytypesFieldContextDTO.trace]: undefined,
|
||||
[TelemetrytypesFieldContextDTO.scope]: undefined,
|
||||
[TelemetrytypesFieldContextDTO.body]: undefined,
|
||||
[TelemetrytypesFieldContextDTO.metric]: undefined,
|
||||
[TelemetrytypesFieldContextDTO.log]: undefined,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
getAllEndpointsWidgetData,
|
||||
getGroupByFiltersFromGroupByValues,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Card } from 'antd';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
|
||||
function MetricOverTimeGraph({
|
||||
widget,
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
getStatusCodeBarChartWidgetData,
|
||||
statusCodeWidgetInfo,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
|
||||
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
|
||||
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
|
||||
import { handleGraphClick } from 'container/WidgetCard/Card/utils';
|
||||
import { useGraphClickToShowButton } from 'container/WidgetCard/hooks/useGraphClickToShowButton';
|
||||
import useNavigateToExplorerPages from 'container/WidgetCard/hooks/useNavigateToExplorerPages';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
@@ -23,7 +23,7 @@ import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import ErrorState from './ErrorState';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ExecStats } from 'api/v5/v5';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
|
||||
@@ -17,7 +17,7 @@ jest.mock('container/ApiMonitoring/utils', () => ({
|
||||
getGroupByFiltersFromGroupByValues: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('container/GridCardLayout/GridCard', () => ({
|
||||
jest.mock('container/WidgetCard/Card', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ customOnRowClick }) => (
|
||||
<div data-testid="grid-card-mock">
|
||||
|
||||
@@ -21,15 +21,12 @@ interface MockQueryResult {
|
||||
}
|
||||
|
||||
// Mocks
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/charts/BarChart/BarChart',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest
|
||||
.fn()
|
||||
.mockImplementation(() => <div data-testid="bar-chart-mock" />),
|
||||
}),
|
||||
);
|
||||
jest.mock('lib/visualization/charts/BarChart/BarChart', () => ({
|
||||
__esModule: true,
|
||||
default: jest
|
||||
.fn()
|
||||
.mockImplementation(() => <div data-testid="bar-chart-mock" />),
|
||||
}));
|
||||
|
||||
jest.mock('components/CeleryTask/useGetGraphCustomSeries', () => ({
|
||||
useGetGraphCustomSeries: (): { getCustomSeries: jest.Mock } => ({
|
||||
@@ -43,7 +40,7 @@ jest.mock('components/CeleryTask/useNavigateToExplorer', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/GridCardLayout/useGraphClickToShowButton', () => ({
|
||||
jest.mock('container/WidgetCard/hooks/useGraphClickToShowButton', () => ({
|
||||
useGraphClickToShowButton: (): {
|
||||
componentClick: boolean;
|
||||
htmlRef: HTMLElement | null;
|
||||
@@ -53,7 +50,7 @@ jest.mock('container/GridCardLayout/useGraphClickToShowButton', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/GridCardLayout/useNavigateToExplorerPages', () => ({
|
||||
jest.mock('container/WidgetCard/hooks/useNavigateToExplorerPages', () => ({
|
||||
__esModule: true,
|
||||
default: (): { navigateToExplorerPages: jest.Mock } => ({
|
||||
navigateToExplorerPages: jest.fn(),
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'components/QuickFilters/types';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { GraphClickMetaData } from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||
import { GraphClickMetaData } from 'container/WidgetCard/hooks/useNavigateToExplorerPages';
|
||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||
import { convertNanoToMilliseconds } from 'container/MetricsExplorer/Summary/utils';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -18,7 +18,7 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { ArrowUpDown, ChevronDown, ChevronRight, Info } from '@signozhq/icons';
|
||||
import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { Card, Flex } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
import { StackMode } from 'lib/uPlotV2/config/types';
|
||||
import {
|
||||
LegendPosition,
|
||||
TooltipRenderArgs,
|
||||
@@ -131,9 +132,9 @@ export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
|
||||
<div ref={graphRef} className={styles.graphContainer}>
|
||||
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
|
||||
<BarChart
|
||||
stack={StackMode.Normal}
|
||||
config={config}
|
||||
data={chartData}
|
||||
isStackedBarChart
|
||||
legendConfig={{ position: LegendPosition.BOTTOM }}
|
||||
customTooltip={renderBillingTooltip}
|
||||
width={containerDimensions.width}
|
||||
|
||||
@@ -58,26 +58,17 @@ describe('prepareBillingBarConfig', () => {
|
||||
expect(config.series?.[4]?.stroke).toBe(Color.BG_AMBER_500);
|
||||
});
|
||||
|
||||
it('sets stacking bands, padding, and focus alpha for behavioral parity', () => {
|
||||
it('sets padding and focus alpha for behavioral parity', () => {
|
||||
const builder = prepareBillingBarConfig({
|
||||
...baseProps,
|
||||
apiResponse: makeApiResponse(['Logs', 'Traces', 'Metrics']),
|
||||
});
|
||||
const config = builder.getConfig();
|
||||
expect(config.bands).toStrictEqual([{ series: [1, 2] }, { series: [2, 3] }]);
|
||||
// Stacking bands come from the chart now — see useChartStacking.
|
||||
expect(config.padding).toStrictEqual([32, 32, 16, 16]);
|
||||
expect(config.focus).toStrictEqual({ alpha: 0.3 });
|
||||
});
|
||||
|
||||
it('sets no bands when result is empty', () => {
|
||||
const builder = prepareBillingBarConfig({
|
||||
...baseProps,
|
||||
apiResponse: makeApiResponse([]),
|
||||
});
|
||||
const config = builder.getConfig();
|
||||
expect(config.bands).toBeUndefined();
|
||||
});
|
||||
|
||||
it('uses queryName as label when legend is undefined', () => {
|
||||
const apiResponse: MetricRangePayloadProps = {
|
||||
data: {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import type { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getInitialStackedBands } from 'container/DashboardContainer/visualization/charts/utils/stackSeriesUtils';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
|
||||
import { DrawStyle } from 'lib/uPlotV2/config/types';
|
||||
import type { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import type { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
@@ -63,7 +62,6 @@ export function prepareBillingBarConfig({
|
||||
});
|
||||
});
|
||||
|
||||
builder.setBands(getInitialStackedBands(results.length));
|
||||
builder.setPadding([32, 32, 16, 16]);
|
||||
builder.setFocus({ alpha: 0.3 });
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ jest.mock('api/channels/getAll', () => ({
|
||||
}));
|
||||
|
||||
// Mock alert format categories
|
||||
jest.mock('container/NewWidget/RightContainer/alertFomatCategories', () => ({
|
||||
jest.mock('constants/formats/alertFormatCategories', () => ({
|
||||
getCategoryByOptionId: jest.fn(() => ({ name: 'bytes' })),
|
||||
getCategorySelectOptionByName: jest.fn(() => [
|
||||
{ label: 'Bytes', value: 'bytes' },
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { useCreateAlertState } from 'container/CreateAlertV2/context';
|
||||
import ChartPreviewComponent from 'container/FormAlertRules/ChartPreview';
|
||||
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
||||
import PlotTag from 'components/PlotTag/PlotTag';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useGetYAxisUnit from 'hooks/useGetYAxisUnit';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
||||
@@ -44,7 +44,7 @@ jest.mock(
|
||||
},
|
||||
);
|
||||
jest.mock(
|
||||
'container/NewWidget/LeftContainer/WidgetGraph/PlotTag',
|
||||
'components/PlotTag/PlotTag',
|
||||
() =>
|
||||
function MockPlotTag(props: any): JSX.Element {
|
||||
return (
|
||||
|
||||
@@ -1,440 +0,0 @@
|
||||
.dashboard-description-container {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
background: unset;
|
||||
box-shadow: none;
|
||||
color: var(--l2-foreground);
|
||||
|
||||
.ant-card-body {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.dashboard-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 16px 16px 0px 16px;
|
||||
align-items: flex-start;
|
||||
|
||||
.left-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 45%;
|
||||
|
||||
.dashboard-img {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px; /* 150% */
|
||||
letter-spacing: -0.08px;
|
||||
max-width: 80%;
|
||||
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.public-dashboard-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.right-section {
|
||||
display: flex;
|
||||
width: 55%;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 32px;
|
||||
height: 34px;
|
||||
padding: 6px;
|
||||
justify-content: center;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 10px; /* 83.333% */
|
||||
letter-spacing: 0.12px;
|
||||
}
|
||||
|
||||
.icons:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
.configure-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 93px;
|
||||
height: 34px;
|
||||
padding: 6px;
|
||||
justify-content: center;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 10px; /* 83.333% */
|
||||
letter-spacing: 0.12px;
|
||||
}
|
||||
|
||||
.add-panel-btn {
|
||||
display: flex;
|
||||
width: 119px;
|
||||
height: 34px;
|
||||
padding: 5.937px 11.875px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--primary-foreground);
|
||||
background: var(--primary-background);
|
||||
font-family: Inter;
|
||||
font-size: 11.875px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 17.812px; /* 150% */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-tags {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 16px 16px 0px 16px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.tag {
|
||||
display: flex;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 20px;
|
||||
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
|
||||
color: var(--bg-sienna-400);
|
||||
text-align: center;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
.dashboard-description-section {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px; /* 157.143% */
|
||||
letter-spacing: -0.07px;
|
||||
padding: 20px 16px 0px 16px;
|
||||
/* Preserve author-entered line breaks in the description. */
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
a {
|
||||
color: var(--accent-primary);
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-variables {
|
||||
padding: 16px 16px 18px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-description {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2; /* Show up to 2 lines */
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-settings {
|
||||
width: 191px;
|
||||
height: 302px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.ant-popover-inner {
|
||||
padding: 0px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
) !important;
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: unset;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
border-top: none;
|
||||
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-1,
|
||||
.section-2 {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.delete-dashboard .ant-btn {
|
||||
color: var(--bg-cherry-400) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rename-dashboard {
|
||||
.ant-modal-content {
|
||||
width: 384px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
padding: 0px;
|
||||
|
||||
.ant-modal-header {
|
||||
height: 52px;
|
||||
padding: 16px;
|
||||
background: var(--l2-background);
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
margin-bottom: 0px;
|
||||
.ant-modal-title {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
width: 349px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 16px;
|
||||
|
||||
.dashboard-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.name-text {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
}
|
||||
|
||||
.dashboard-name-input {
|
||||
display: flex;
|
||||
padding: 6px 6px 6px 8px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
align-self: stretch;
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
padding: 16px;
|
||||
margin-top: 0px;
|
||||
.dashboard-rename {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
gap: 12px;
|
||||
|
||||
.cancel-btn {
|
||||
display: flex;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--l1-border);
|
||||
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.rename-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
width: 169px;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--primary-background);
|
||||
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-naming {
|
||||
.ant-modal-content {
|
||||
width: 384px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
padding: 0px;
|
||||
|
||||
.ant-modal-header {
|
||||
height: 52px;
|
||||
padding: 16px;
|
||||
background: var(--l2-background);
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
margin-bottom: 0px;
|
||||
.ant-modal-title {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
width: 349px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 16px;
|
||||
|
||||
.section-naming-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.name-text {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
}
|
||||
|
||||
.section-name-input {
|
||||
display: flex;
|
||||
width: 320px;
|
||||
padding: 6px 6px 6px 8px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
align-self: stretch;
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
padding: 16px;
|
||||
margin-top: 0px;
|
||||
.dashboard-rename {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
gap: 12px;
|
||||
|
||||
.cancel-btn {
|
||||
display: flex;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--l1-border);
|
||||
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.rename-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
width: 140px;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--primary-background);
|
||||
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
.settings-container-root {
|
||||
.ant-drawer-wrapper-body {
|
||||
border-left: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.ant-drawer-header {
|
||||
height: 48px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
padding: 14px 14px 14px 11px;
|
||||
|
||||
.ant-drawer-header-title {
|
||||
gap: 16px;
|
||||
|
||||
.ant-drawer-title {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-inline-end: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding: 16px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { memo, PropsWithChildren, ReactElement } from 'react';
|
||||
import { Drawer } from 'antd';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
|
||||
import './SettingsDrawer.styles.scss';
|
||||
|
||||
type SettingsDrawerProps = PropsWithChildren<{
|
||||
drawerTitle: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}>;
|
||||
|
||||
function SettingsDrawer({
|
||||
children,
|
||||
drawerTitle,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: SettingsDrawerProps): JSX.Element {
|
||||
return (
|
||||
<Drawer
|
||||
title={drawerTitle}
|
||||
placement="right"
|
||||
width="50%"
|
||||
onClose={onClose}
|
||||
open={isOpen}
|
||||
rootClassName="settings-container-root"
|
||||
>
|
||||
{/* Need to type cast because of OverlayScrollbar type definition. We should be good once we remove it. */}
|
||||
<OverlayScrollbar>{children as ReactElement}</OverlayScrollbar>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(SettingsDrawer);
|
||||
@@ -1,235 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||
import { useDashboardBootstrap } from 'hooks/dashboard/useDashboardBootstrap';
|
||||
import {
|
||||
getDashboardById,
|
||||
getNonIntegrationDashboardById,
|
||||
} from 'mocks-server/__mockdata__/dashboards';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import {
|
||||
resetDashboard,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'tests/test-utils';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
import DashboardDescription from '..';
|
||||
|
||||
function DashboardBootstrapWrapper({
|
||||
dashboardId,
|
||||
children,
|
||||
}: {
|
||||
dashboardId: string;
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
useDashboardBootstrap(dashboardId);
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
interface MockSafeNavigateReturn {
|
||||
safeNavigate: jest.MockedFunction<(url: string) => void>;
|
||||
}
|
||||
|
||||
const DASHBOARD_TEST_ID = 'dashboard-title';
|
||||
const DASHBOARD_TITLE_TEXT = 'thor';
|
||||
const DASHBOARD_PATH = '/dashboard/4';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/TopNav/DateTimeSelectionV2/index.tsx',
|
||||
() =>
|
||||
function MockDateTimeSelection(): JSX.Element {
|
||||
return <div>MockDateTimeSelection</div>;
|
||||
},
|
||||
);
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): MockSafeNavigateReturn => ({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Dashboard landing page actions header tests', () => {
|
||||
beforeEach(() => {
|
||||
mockSafeNavigate.mockClear();
|
||||
sessionStorage.clear();
|
||||
resetDashboard();
|
||||
});
|
||||
|
||||
it('unlock dashboard should be disabled for integrations created dashboards', async () => {
|
||||
const mockLocation = {
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${DASHBOARD_PATH}`,
|
||||
search: '',
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
const { getByTestId } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardBootstrapWrapper dashboardId="4">
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
enter: (): Promise<void> => Promise.resolve(),
|
||||
exit: (): Promise<void> => Promise.resolve(),
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardBootstrapWrapper>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByTestId(DASHBOARD_TEST_ID)).toHaveTextContent(
|
||||
DASHBOARD_TITLE_TEXT,
|
||||
),
|
||||
);
|
||||
|
||||
const dashboardSettingsTrigger = getByTestId('options');
|
||||
|
||||
await fireEvent.click(dashboardSettingsTrigger);
|
||||
|
||||
const lockUnlockButton = screen.getByTestId('lock-unlock-dashboard');
|
||||
|
||||
await waitFor(() => expect(lockUnlockButton).toBeDisabled());
|
||||
});
|
||||
|
||||
it('unlock dashboard should not be disabled for non integration created dashboards', async () => {
|
||||
const mockLocation = {
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${DASHBOARD_PATH}`,
|
||||
search: '',
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/dashboards/4', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(getNonIntegrationDashboardById)),
|
||||
),
|
||||
);
|
||||
const { getByTestId } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardBootstrapWrapper dashboardId="4">
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
enter: (): Promise<void> => Promise.resolve(),
|
||||
exit: (): Promise<void> => Promise.resolve(),
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardBootstrapWrapper>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByTestId(DASHBOARD_TEST_ID)).toHaveTextContent(
|
||||
DASHBOARD_TITLE_TEXT,
|
||||
),
|
||||
);
|
||||
|
||||
const dashboardSettingsTrigger = getByTestId('options');
|
||||
|
||||
await fireEvent.click(dashboardSettingsTrigger);
|
||||
|
||||
const lockUnlockButton = screen.getByTestId('lock-unlock-dashboard');
|
||||
|
||||
await waitFor(() => expect(lockUnlockButton).not.toBeDisabled());
|
||||
});
|
||||
|
||||
it('should navigate to base dashboard list URL when no saved params exist', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockLocation = {
|
||||
pathname: DASHBOARD_PATH,
|
||||
search: '',
|
||||
};
|
||||
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
|
||||
const { getByText } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardBootstrapWrapper dashboardId="4">
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
enter: (): Promise<void> => Promise.resolve(),
|
||||
exit: (): Promise<void> => Promise.resolve(),
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardBootstrapWrapper>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId(DASHBOARD_TEST_ID)).toHaveTextContent(
|
||||
DASHBOARD_TITLE_TEXT,
|
||||
),
|
||||
);
|
||||
|
||||
const dashboardButton = getByText('Dashboard /');
|
||||
await user.click(dashboardButton);
|
||||
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith('/dashboard');
|
||||
});
|
||||
|
||||
it('should navigate to dashboard list with saved query params when present', async () => {
|
||||
const user = userEvent.setup();
|
||||
const savedParams = 'columnKey=createdAt&order=ascend&page=2&search=foo';
|
||||
sessionStorage.setItem('dashboardsListQueryParams', savedParams);
|
||||
|
||||
const mockLocation = {
|
||||
pathname: DASHBOARD_PATH,
|
||||
search: '',
|
||||
};
|
||||
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
|
||||
useDashboardStore.setState({
|
||||
dashboardData: getDashboardById.data as unknown as Dashboard,
|
||||
layouts: [],
|
||||
panelMap: {},
|
||||
setPanelMap: jest.fn(),
|
||||
setLayouts: jest.fn(),
|
||||
setDashboardData: jest.fn(),
|
||||
columnWidths: {},
|
||||
});
|
||||
|
||||
const { getByText } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
enter: (): Promise<void> => Promise.resolve(),
|
||||
exit: (): Promise<void> => Promise.resolve(),
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId(DASHBOARD_TEST_ID)).toHaveTextContent(
|
||||
DASHBOARD_TITLE_TEXT,
|
||||
),
|
||||
);
|
||||
|
||||
const dashboardButton = getByText('Dashboard /');
|
||||
await user.click(dashboardButton);
|
||||
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith({
|
||||
pathname: '/dashboard',
|
||||
search: `?${savedParams}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,621 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { FullScreenHandle } from 'react-full-screen';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import {
|
||||
Check,
|
||||
ClipboardCopy,
|
||||
Ellipsis,
|
||||
FileJson,
|
||||
FolderKanban,
|
||||
Fullscreen,
|
||||
Globe,
|
||||
LockKeyhole,
|
||||
PenLine,
|
||||
Plus,
|
||||
X,
|
||||
} from '@signozhq/icons';
|
||||
import { Button, Card, Modal, Popover, Tooltip } from 'antd';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteButton';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta';
|
||||
import { useLockDashboard } from 'hooks/dashboard/useLockDashboard';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { sortLayout } from 'providers/Dashboard/util';
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { linkifyText } from 'utils/linkifyText';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import DashboardHeader from '../components/DashboardHeader/DashboardHeader';
|
||||
import DashboardSettings from '../DashboardSettings';
|
||||
import { Base64Icons } from '../DashboardSettings/General/utils';
|
||||
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
||||
import PanelTypeSelectionModal from '../PanelTypeSelectionModal';
|
||||
import SettingsDrawer from './SettingsDrawer';
|
||||
import { VariablesSettingsTab } from './types';
|
||||
import {
|
||||
DEFAULT_ROW_NAME,
|
||||
downloadObjectAsJson,
|
||||
sanitizeDashboardData,
|
||||
} from './utils';
|
||||
|
||||
import './Description.styles.scss';
|
||||
|
||||
interface DashboardDescriptionProps {
|
||||
handle: FullScreenHandle;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
const { handle } = props;
|
||||
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||
);
|
||||
const {
|
||||
dashboardData,
|
||||
panelMap,
|
||||
setPanelMap,
|
||||
layouts,
|
||||
setLayouts,
|
||||
setDashboardData,
|
||||
} = useDashboardStore();
|
||||
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
const handleDashboardLockToggle = useLockDashboard();
|
||||
|
||||
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
|
||||
const isPublicDashboardEnabled = isCloudUser || isEnterpriseSelfHostedUser;
|
||||
|
||||
const selectedData = dashboardData
|
||||
? {
|
||||
...dashboardData.data,
|
||||
uuid: dashboardData.id,
|
||||
}
|
||||
: ({} as DashboardData);
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
|
||||
const {
|
||||
title = '',
|
||||
description,
|
||||
tags,
|
||||
image = Base64Icons[0],
|
||||
} = selectedData || {};
|
||||
|
||||
const [updatedTitle, setUpdatedTitle] = useState<string>(title);
|
||||
|
||||
const [sectionName, setSectionName] = useState<string>(DEFAULT_ROW_NAME);
|
||||
|
||||
const updateDashboardMutation = useUpdateDashboard();
|
||||
|
||||
const { user } = useAppContext();
|
||||
const [editDashboard] = useComponentPermission(['edit_dashboard'], user.role);
|
||||
const [isDashboardSettingsOpen, setIsDashbordSettingsOpen] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [isRenameDashboardOpen, setIsRenameDashboardOpen] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [isPanelNameModalOpen, setIsPanelNameModalOpen] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [isPublicDashboard, setIsPublicDashboard] = useState<boolean>(false);
|
||||
|
||||
let isAuthor = false;
|
||||
|
||||
if (dashboardData && user && user.email) {
|
||||
isAuthor = dashboardData?.createdBy === user?.email;
|
||||
}
|
||||
|
||||
let permissions: ComponentTypes[] = ['add_panel'];
|
||||
|
||||
if (isDashboardLocked) {
|
||||
permissions = ['add_panel_locked_dashboard'];
|
||||
}
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const userRole: ROLES | null =
|
||||
dashboardData?.createdBy === user?.email
|
||||
? (USER_ROLES.AUTHOR as ROLES)
|
||||
: user.role;
|
||||
|
||||
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
||||
|
||||
const onEmptyWidgetHandler = useCallback(() => {
|
||||
setIsPanelTypeSelectionModalOpen(true);
|
||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||
dashboardId: dashboardData?.id,
|
||||
dashboardName: dashboardData?.data.title,
|
||||
numberOfPanels: dashboardData?.data.widgets?.length,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [setIsPanelTypeSelectionModalOpen]);
|
||||
|
||||
const handleLockDashboardToggle = (): void => {
|
||||
setIsDashbordSettingsOpen(false);
|
||||
handleDashboardLockToggle(!isDashboardLocked);
|
||||
};
|
||||
|
||||
const onNameChangeHandler = (): void => {
|
||||
if (!dashboardData) {
|
||||
return;
|
||||
}
|
||||
const updatedDashboard: Props = {
|
||||
id: dashboardData.id,
|
||||
|
||||
data: {
|
||||
...dashboardData.data,
|
||||
title: updatedTitle,
|
||||
},
|
||||
};
|
||||
updateDashboardMutation.mutate(updatedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
notifications.success({
|
||||
message: 'Dashboard renamed successfully',
|
||||
});
|
||||
setIsRenameDashboardOpen(false);
|
||||
if (updatedDashboard.data) {
|
||||
setDashboardData(updatedDashboard.data);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
setIsRenameDashboardOpen(true);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const [state, setCopy] = useCopyToClipboard();
|
||||
|
||||
const { t } = useTranslation(['dashboard', 'common']);
|
||||
|
||||
// used to set the initial value for the updatedTitle
|
||||
// the context value is sometimes not available during the initial render
|
||||
// due to which the updatedTitle is set to some previous value
|
||||
useEffect(() => {
|
||||
if (dashboardData) {
|
||||
setUpdatedTitle(dashboardData.data.title);
|
||||
}
|
||||
}, [dashboardData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.error) {
|
||||
notifications.error({
|
||||
message: t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (state.value) {
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
}, [state.error, state.value, t, notifications]);
|
||||
|
||||
function handleAddRow(): void {
|
||||
if (!dashboardData) {
|
||||
return;
|
||||
}
|
||||
const id = uuid();
|
||||
|
||||
const newRowWidgetMap: { widgets: Layout[]; collapsed: boolean } = {
|
||||
widgets: [],
|
||||
collapsed: false,
|
||||
};
|
||||
const currentRowIdx = 0;
|
||||
for (let j = currentRowIdx; j < layouts.length; j++) {
|
||||
if (!panelMap[layouts[j].i]) {
|
||||
newRowWidgetMap.widgets.push(layouts[j]);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const updatedDashboard: Props = {
|
||||
id: dashboardData.id,
|
||||
|
||||
data: {
|
||||
...dashboardData.data,
|
||||
layout: [
|
||||
{
|
||||
i: id,
|
||||
w: 12,
|
||||
minW: 12,
|
||||
minH: 1,
|
||||
maxH: 1,
|
||||
x: 0,
|
||||
h: 1,
|
||||
y: 0,
|
||||
},
|
||||
...layouts.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
|
||||
],
|
||||
panelMap: { ...panelMap, [id]: newRowWidgetMap },
|
||||
widgets: [
|
||||
...(dashboardData.data.widgets || []),
|
||||
{
|
||||
id,
|
||||
title: sectionName,
|
||||
description: '',
|
||||
panelTypes: PANEL_GROUP_TYPES.ROW,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutate(updatedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (updatedDashboard.data) {
|
||||
if (updatedDashboard.data.data.layout) {
|
||||
setLayouts(sortLayout(updatedDashboard.data.data.layout));
|
||||
}
|
||||
setDashboardData(updatedDashboard.data);
|
||||
setPanelMap(updatedDashboard.data?.data?.panelMap || {});
|
||||
}
|
||||
|
||||
setIsPanelNameModalOpen(false);
|
||||
setSectionName(DEFAULT_ROW_NAME);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: publicDashboardResponse,
|
||||
isLoading: isLoadingPublicDashboardData,
|
||||
isFetching: isFetchingPublicDashboardData,
|
||||
error: errorPublicDashboardData,
|
||||
isError: isErrorPublicDashboardData,
|
||||
} = useGetPublicDashboardMeta(
|
||||
dashboardData?.id || '',
|
||||
!!dashboardData?.id && isPublicDashboardEnabled,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingPublicDashboardData && !isFetchingPublicDashboardData) {
|
||||
if (isErrorPublicDashboardData) {
|
||||
const errorDetails = errorPublicDashboardData?.getErrorDetails();
|
||||
|
||||
if (errorDetails?.error?.code === 'public_dashboard_not_found') {
|
||||
setIsPublicDashboard(false);
|
||||
}
|
||||
} else {
|
||||
const publicDashboardData = publicDashboardResponse?.data;
|
||||
if (publicDashboardData?.publicPath) {
|
||||
setIsPublicDashboard(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isLoadingPublicDashboardData,
|
||||
isFetchingPublicDashboardData,
|
||||
isErrorPublicDashboardData,
|
||||
errorPublicDashboardData,
|
||||
publicDashboardResponse?.data,
|
||||
]);
|
||||
|
||||
const onConfigureClick = useCallback((): void => {
|
||||
setIsSettingsDrawerOpen(true);
|
||||
}, []);
|
||||
|
||||
const onSettingsDrawerClose = useCallback((): void => {
|
||||
setIsSettingsDrawerOpen(false);
|
||||
// good use case for a state library like Jotai
|
||||
if (variablesSettingsTabHandle.current) {
|
||||
variablesSettingsTabHandle.current.resetState();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card className="dashboard-description-container">
|
||||
<DashboardHeader />
|
||||
<section className="dashboard-details">
|
||||
<div className="left-section">
|
||||
<img src={image} alt="dashboard-img" className="dashboard-img" />
|
||||
<Tooltip title={title.length > 30 ? title : ''}>
|
||||
<Typography.Text
|
||||
className="dashboard-title"
|
||||
data-testid="dashboard-title"
|
||||
>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
|
||||
{isPublicDashboard && (
|
||||
<Tooltip title="This dashboard is publicly accessible">
|
||||
<Globe size={14} className="public-dashboard-icon" />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isDashboardLocked && (
|
||||
<Tooltip title="This dashboard is locked">
|
||||
<LockKeyhole size={14} className="lock-dashboard-icon" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="right-section">
|
||||
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
|
||||
<Popover
|
||||
open={isDashboardSettingsOpen}
|
||||
arrow={false}
|
||||
onOpenChange={(visible): void => setIsDashbordSettingsOpen(visible)}
|
||||
rootClassName="dashboard-settings"
|
||||
content={
|
||||
<div className="menu-content">
|
||||
<section className="section-1">
|
||||
{(isAuthor || user.role === USER_ROLES.ADMIN) && (
|
||||
<Tooltip
|
||||
title={
|
||||
dashboardData?.createdBy === 'integration' &&
|
||||
'Dashboards created by integrations cannot be unlocked'
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<LockKeyhole size={14} />}
|
||||
disabled={dashboardData?.createdBy === 'integration'}
|
||||
onClick={handleLockDashboardToggle}
|
||||
data-testid="lock-unlock-dashboard"
|
||||
>
|
||||
{isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!isDashboardLocked && editDashboard && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PenLine size={14} />}
|
||||
onClick={(): void => {
|
||||
setIsRenameDashboardOpen(true);
|
||||
setIsDashbordSettingsOpen(false);
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Fullscreen size={14} />}
|
||||
onClick={handle.enter}
|
||||
>
|
||||
Full screen
|
||||
</Button>
|
||||
</section>
|
||||
<section className="section-2">
|
||||
{!isDashboardLocked && addPanelPermission && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<FolderKanban size={14} />}
|
||||
onClick={(): void => {
|
||||
setIsPanelNameModalOpen(true);
|
||||
setIsDashbordSettingsOpen(false);
|
||||
}}
|
||||
>
|
||||
New section
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="text"
|
||||
icon={<FileJson size={14} />}
|
||||
onClick={(): void => {
|
||||
downloadObjectAsJson(
|
||||
sanitizeDashboardData(selectedData),
|
||||
selectedData.title,
|
||||
);
|
||||
setIsDashbordSettingsOpen(false);
|
||||
}}
|
||||
>
|
||||
Export JSON
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ClipboardCopy size={14} />}
|
||||
onClick={(): void => {
|
||||
setCopy(
|
||||
JSON.stringify(sanitizeDashboardData(selectedData), null, 2),
|
||||
);
|
||||
setIsDashbordSettingsOpen(false);
|
||||
}}
|
||||
>
|
||||
Copy as JSON
|
||||
</Button>
|
||||
</section>
|
||||
<section className="delete-dashboard">
|
||||
<DeleteButton
|
||||
createdBy={dashboardData?.createdBy || ''}
|
||||
name={dashboardData?.data.title || ''}
|
||||
id={String(dashboardData?.id) || ''}
|
||||
isLocked={isDashboardLocked}
|
||||
routeToListPage
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button
|
||||
icon={<Ellipsis size={14} />}
|
||||
type="text"
|
||||
className="icons"
|
||||
data-testid="options"
|
||||
/>
|
||||
</Popover>
|
||||
{!isDashboardLocked && editDashboard && (
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
className="configure-button"
|
||||
icon={<ConfigureIcon />}
|
||||
data-testid="show-drawer"
|
||||
onClick={onConfigureClick}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
<SettingsDrawer
|
||||
drawerTitle="Dashboard Configuration"
|
||||
isOpen={isSettingsDrawerOpen}
|
||||
onClose={onSettingsDrawerClose}
|
||||
>
|
||||
<DashboardSettings
|
||||
variablesSettingsTabHandle={variablesSettingsTabHandle}
|
||||
/>
|
||||
</SettingsDrawer>
|
||||
</>
|
||||
)}
|
||||
{!isDashboardLocked && addPanelPermission && (
|
||||
<Button
|
||||
className="add-panel-btn"
|
||||
onClick={onEmptyWidgetHandler}
|
||||
icon={<Plus size="md" />}
|
||||
type="primary"
|
||||
data-testid="add-panel-header"
|
||||
>
|
||||
New Panel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
{(tags?.length || 0) > 0 && (
|
||||
<div className="dashboard-tags">
|
||||
{tags?.map((tag) => (
|
||||
<Badge key={tag} className="tag" color="vanilla">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!isEmpty(description) && (
|
||||
<section className="dashboard-description-section">
|
||||
{linkifyText(description ?? '')}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{!isEmpty(dashboardVariables) && (
|
||||
<section className="dashboard-variables">
|
||||
<DashboardVariableSelection />
|
||||
</section>
|
||||
)}
|
||||
<PanelTypeSelectionModal />
|
||||
|
||||
<Modal
|
||||
open={isRenameDashboardOpen}
|
||||
title="Rename Dashboard"
|
||||
onOk={(): void => {
|
||||
// handle update dashboard here
|
||||
}}
|
||||
onCancel={(): void => {
|
||||
setIsRenameDashboardOpen(false);
|
||||
}}
|
||||
rootClassName="rename-dashboard"
|
||||
footer={
|
||||
<div className="dashboard-rename">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Check size={14} />}
|
||||
className="rename-btn"
|
||||
onClick={onNameChangeHandler}
|
||||
disabled={updateDashboardMutation.isLoading}
|
||||
>
|
||||
Rename Dashboard
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<X size={14} />}
|
||||
className="cancel-btn"
|
||||
onClick={(): void => setIsRenameDashboardOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="dashboard-content">
|
||||
<Typography.Text className="name-text">Enter a new name</Typography.Text>
|
||||
<Input
|
||||
data-testid="dashboard-name"
|
||||
className="dashboard-name-input"
|
||||
value={updatedTitle}
|
||||
onChange={(e): void => setUpdatedTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
open={isPanelNameModalOpen}
|
||||
title="New Section"
|
||||
rootClassName="section-naming"
|
||||
onOk={(): void => handleAddRow()}
|
||||
onCancel={(): void => {
|
||||
setIsPanelNameModalOpen(false);
|
||||
setSectionName(DEFAULT_ROW_NAME);
|
||||
}}
|
||||
footer={
|
||||
<div className="dashboard-rename">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Check size={14} />}
|
||||
className="rename-btn"
|
||||
onClick={(): void => handleAddRow()}
|
||||
disabled={updateDashboardMutation.isLoading}
|
||||
>
|
||||
Create Section
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<X size={14} />}
|
||||
className="cancel-btn"
|
||||
onClick={(): void => {
|
||||
setIsPanelNameModalOpen(false);
|
||||
setSectionName(DEFAULT_ROW_NAME);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="section-naming-content">
|
||||
<Typography.Text className="name-text">Enter Section name</Typography.Text>
|
||||
<Input
|
||||
data-testid="section-name"
|
||||
className="section-name-input"
|
||||
value={sectionName}
|
||||
onChange={(e): void => setSectionName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardDescription;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { MutableRefObject } from 'react';
|
||||
|
||||
export interface VariablesSettingsTab {
|
||||
resetState: () => void;
|
||||
}
|
||||
|
||||
export type VariablesSettingsTabHandle =
|
||||
MutableRefObject<VariablesSettingsTab | null>;
|
||||
@@ -1,40 +0,0 @@
|
||||
import { DashboardData, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
export function sanitizeDashboardData(
|
||||
selectedData: DashboardData,
|
||||
): DashboardData {
|
||||
if (!selectedData?.variables) {
|
||||
return selectedData;
|
||||
}
|
||||
|
||||
const updatedVariables = Object.entries(selectedData.variables).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const { selectedValue: _selectedValue, ...rest } = value;
|
||||
acc[key] = rest;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, IDashboardVariable>,
|
||||
);
|
||||
|
||||
return {
|
||||
...selectedData,
|
||||
variables: updatedVariables,
|
||||
};
|
||||
}
|
||||
|
||||
export function downloadObjectAsJson(
|
||||
exportObj: unknown,
|
||||
exportName: string,
|
||||
): void {
|
||||
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(
|
||||
JSON.stringify(exportObj),
|
||||
)}`;
|
||||
const downloadAnchorNode = document.createElement('a');
|
||||
downloadAnchorNode.setAttribute('href', dataStr);
|
||||
downloadAnchorNode.setAttribute('download', `${exportName}.json`);
|
||||
document.body.appendChild(downloadAnchorNode); // required for firefox
|
||||
downloadAnchorNode.click();
|
||||
downloadAnchorNode.remove();
|
||||
}
|
||||
|
||||
export const DEFAULT_ROW_NAME = 'Sample Row';
|
||||
@@ -1,123 +0,0 @@
|
||||
.delete-variable-name {
|
||||
font-weight: 700;
|
||||
color: rgb(207, 19, 34);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.apply-to-all-variable-name {
|
||||
font-weight: 700;
|
||||
color: var(--bg-robin-400);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dashboard-variable-settings-table {
|
||||
.variable-name-drag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.ant-table-cell {
|
||||
padding: 0px !important;
|
||||
border: none !important;
|
||||
color: var(--bg-robin-400);
|
||||
font-family: 'Space Mono';
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
.variable-description-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1 0 0;
|
||||
|
||||
.variable-description {
|
||||
color: var(--bg-sienna-400);
|
||||
font-family: 'Space Mono';
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.actions-btns {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
display: none;
|
||||
|
||||
&:hover {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.edit-variable-button {
|
||||
width: 26px;
|
||||
height: 22px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
padding: 4px 6px;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
background: var(--l1-border);
|
||||
}
|
||||
|
||||
.delete-variable-button {
|
||||
width: 26px;
|
||||
height: 22px;
|
||||
border-radius: 2px;
|
||||
background: color-mix(in srgb, var(--danger-background) 10%, transparent);
|
||||
display: flex;
|
||||
padding: 4px 6px;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.apply-to-all-button {
|
||||
width: min-content;
|
||||
height: 22px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
padding: 0px 6px;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
background: var(--l1-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell-row-hover {
|
||||
.actions-btns {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-thead {
|
||||
.ant-table-cell {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: unset;
|
||||
}
|
||||
|
||||
.ant-table-cell::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-tbody {
|
||||
.ant-table-cell {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-row {
|
||||
.ant-table-cell:nth-child(even) {
|
||||
background: color-mix(in srgb, var(--l1-border) 40%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||