mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-20 11:40:44 +01:00
Compare commits
4 Commits
test/semco
...
nv/area-ch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e637bbba02 | ||
|
|
dca6aa497d | ||
|
|
bb47550c01 | ||
|
|
ecf510ed67 |
@@ -2646,6 +2646,54 @@ components:
|
||||
repeatVariable:
|
||||
type: string
|
||||
type: object
|
||||
DashboardtypesAreaChartAppearance:
|
||||
properties:
|
||||
fillMode:
|
||||
$ref: '#/components/schemas/DashboardtypesAreaFillMode'
|
||||
fillOpacity:
|
||||
$ref: '#/components/schemas/DashboardtypesFillOpacity'
|
||||
lineInterpolation:
|
||||
$ref: '#/components/schemas/DashboardtypesLineInterpolation'
|
||||
lineStyle:
|
||||
$ref: '#/components/schemas/DashboardtypesLineStyle'
|
||||
showPoints:
|
||||
type: boolean
|
||||
spanGaps:
|
||||
$ref: '#/components/schemas/DashboardtypesSpanGaps'
|
||||
type: object
|
||||
DashboardtypesAreaChartPanelSpec:
|
||||
properties:
|
||||
axes:
|
||||
$ref: '#/components/schemas/DashboardtypesAxes'
|
||||
chartAppearance:
|
||||
$ref: '#/components/schemas/DashboardtypesAreaChartAppearance'
|
||||
formatting:
|
||||
$ref: '#/components/schemas/DashboardtypesPanelFormatting'
|
||||
legend:
|
||||
$ref: '#/components/schemas/DashboardtypesLegend'
|
||||
thresholds:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardtypesThresholdWithLabel'
|
||||
nullable: true
|
||||
type: array
|
||||
visualization:
|
||||
$ref: '#/components/schemas/DashboardtypesAreaChartVisualization'
|
||||
type: object
|
||||
DashboardtypesAreaChartVisualization:
|
||||
properties:
|
||||
fillSpans:
|
||||
type: boolean
|
||||
stack:
|
||||
$ref: '#/components/schemas/DashboardtypesStackMode'
|
||||
timePreference:
|
||||
$ref: '#/components/schemas/DashboardtypesTimePreference'
|
||||
type: object
|
||||
DashboardtypesAreaFillMode:
|
||||
enum:
|
||||
- solid
|
||||
- gradient
|
||||
- none
|
||||
type: string
|
||||
DashboardtypesAxes:
|
||||
properties:
|
||||
isLogScale:
|
||||
@@ -2877,6 +2925,11 @@ components:
|
||||
- gradient
|
||||
- none
|
||||
type: string
|
||||
DashboardtypesFillOpacity:
|
||||
maximum: 1
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: number
|
||||
DashboardtypesGettableDashboardV2:
|
||||
properties:
|
||||
createdAt:
|
||||
@@ -3294,6 +3347,7 @@ components:
|
||||
DashboardtypesPanelPlugin:
|
||||
discriminator:
|
||||
mapping:
|
||||
signoz/AreaChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec'
|
||||
signoz/BarChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec'
|
||||
signoz/HistogramPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec'
|
||||
signoz/ListPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec'
|
||||
@@ -3305,6 +3359,7 @@ components:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec'
|
||||
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec'
|
||||
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec'
|
||||
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpec'
|
||||
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpec'
|
||||
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec'
|
||||
@@ -3315,12 +3370,25 @@ components:
|
||||
enum:
|
||||
- signoz/TimeSeriesPanel
|
||||
- signoz/BarChartPanel
|
||||
- signoz/AreaChartPanel
|
||||
- signoz/NumberPanel
|
||||
- signoz/PieChartPanel
|
||||
- signoz/TablePanel
|
||||
- signoz/HistogramPanel
|
||||
- signoz/ListPanel
|
||||
type: string
|
||||
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/AreaChartPanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/DashboardtypesAreaChartPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
@@ -3639,6 +3707,12 @@ components:
|
||||
are connected.
|
||||
type: boolean
|
||||
type: object
|
||||
DashboardtypesStackMode:
|
||||
enum:
|
||||
- none
|
||||
- normal
|
||||
- percent
|
||||
type: string
|
||||
DashboardtypesStorableDashboardData:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
@@ -8024,13 +8098,6 @@ components:
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
ServiceaccounttypesGettableFactorAPIKey:
|
||||
properties:
|
||||
createdAt:
|
||||
@@ -13262,117 +13329,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
|
||||
@@ -14376,9 +14332,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
|
||||
@@ -14438,9 +14394,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
|
||||
@@ -14484,9 +14444,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- auth-domain:delete
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- auth-domain:delete
|
||||
summary: Delete auth domain
|
||||
tags:
|
||||
- authdomains
|
||||
@@ -14541,9 +14501,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
|
||||
@@ -14597,9 +14557,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9092,13 +9092,6 @@ export interface SavedviewtypesUpdatableSavedViewDTO {
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -11020,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,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;
|
||||
@@ -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,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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { memo, useMemo } from 'react';
|
||||
import { Select, Spin } from 'antd';
|
||||
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
||||
import { useOrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter/useOrderByFilter';
|
||||
import { selectStyle } from 'container/QueryBuilder/filters/QueryBuilderSearch/config';
|
||||
import { selectStyle } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/config';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { StringOperators } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
RESTRICTED_SELECTED_FIELDS,
|
||||
} from 'container/LogsFilters/config';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
|
||||
@@ -42,7 +42,7 @@ jest.mock('hooks/useDebounce', () => ({
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/QueryBuilder/filters/QueryBuilderSearch/OptionRenderer',
|
||||
'container/QueryBuilder/filters/OptionRenderer/OptionRenderer',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: ({ value }: { value: string }): JSX.Element => <span>{value}</span>,
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { OrderByFilterProps } from './filters/OrderByFilter/OrderByFilter.interfaces';
|
||||
|
||||
export type WhereClauseConfig = {
|
||||
customKey: string;
|
||||
customOp: string;
|
||||
};
|
||||
|
||||
type FilterConfigs = {
|
||||
[Key in keyof Omit<IBuilderQuery, 'filters'>]: {
|
||||
isHidden: boolean;
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { InputNumber, InputNumberProps } from 'antd';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
|
||||
function AggregateEveryFilter({
|
||||
onChange,
|
||||
query,
|
||||
disabled,
|
||||
}: AggregateEveryFilterProps): JSX.Element {
|
||||
const isMetricsDataSource = useMemo(
|
||||
() => query.dataSource === DataSource.METRICS,
|
||||
[query.dataSource],
|
||||
);
|
||||
|
||||
const onChangeHandler: InputNumberProps<number>['onChange'] = (event) => {
|
||||
if (event && event >= 0) {
|
||||
onChange(event);
|
||||
}
|
||||
};
|
||||
|
||||
const isDisabled =
|
||||
(isMetricsDataSource && !query.aggregateAttribute?.key) || disabled;
|
||||
|
||||
return (
|
||||
<InputNumber
|
||||
placeholder="Enter in seconds"
|
||||
disabled={isDisabled}
|
||||
style={selectStyle}
|
||||
value={query?.stepInterval}
|
||||
onChange={onChangeHandler}
|
||||
min={0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface AggregateEveryFilterProps {
|
||||
onChange: (values: number) => void;
|
||||
query: IBuilderQuery;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export default AggregateEveryFilter;
|
||||
@@ -24,8 +24,8 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { ExtendedSelectOption } from 'types/common/select';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
import OptionRenderer from '../QueryBuilderSearch/OptionRenderer';
|
||||
import { selectStyle } from '../QueryBuilderSearchV2/config';
|
||||
import OptionRenderer from '../OptionRenderer/OptionRenderer';
|
||||
// ** Types
|
||||
import { AgregatorFilterProps } from './AggregatorFilter.intefaces';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { InputNumber } from 'antd';
|
||||
|
||||
import { selectStyle } from '../../QueryBuilderSearch/config';
|
||||
import { selectStyle } from '../../QueryBuilderSearchV2/config';
|
||||
import { handleKeyDownLimitFilter } from '../../utils';
|
||||
import { LimitFilterProps } from './types';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { selectStyle } from '../../QueryBuilderSearch/config';
|
||||
import { selectStyle } from '../../QueryBuilderSearchV2/config';
|
||||
import { OrderByProps } from './types';
|
||||
import { useOrderByFormulaFilter } from './useOrderByFormulaFilter';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
mapLabelValuePairs,
|
||||
orderByValueDelimiter,
|
||||
} from '../../OrderByFilter/utils';
|
||||
import { getRemoveOrderFromValue } from '../../QueryBuilderSearch/utils';
|
||||
import { getRemoveOrderFromValue } from '../../QueryBuilderSearchV2/utils';
|
||||
import { getUniqueOrderByValues, getValidOrderByResult } from '../../utils';
|
||||
import { IOrderByFormulaFilterProps } from './types';
|
||||
import { transformToOrderByStringValuesByFormula } from './utils';
|
||||
|
||||
@@ -23,8 +23,8 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { SelectOption } from 'types/common/select';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
import OptionRenderer from '../QueryBuilderSearch/OptionRenderer';
|
||||
import { selectStyle } from '../QueryBuilderSearchV2/config';
|
||||
import OptionRenderer from '../OptionRenderer/OptionRenderer';
|
||||
import { GroupByFilterProps } from './GroupByFilter.interfaces';
|
||||
|
||||
export const GroupByFilter = memo(function GroupByFilter({
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { InputNumber } from 'antd';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
import { handleKeyDownLimitFilter } from '../utils';
|
||||
|
||||
function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
|
||||
const isMetricsDataSource = query.dataSource === DataSource.METRICS;
|
||||
|
||||
const isDisabled = isMetricsDataSource && !query.aggregateAttribute?.key;
|
||||
|
||||
return (
|
||||
<InputNumber
|
||||
min={1}
|
||||
type="number"
|
||||
value={query.limit}
|
||||
style={selectStyle}
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
onKeyDown={handleKeyDownLimitFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface LimitFilterProps {
|
||||
onChange: (values: number | null) => void;
|
||||
query: IBuilderQuery;
|
||||
}
|
||||
|
||||
export default LimitFilter;
|
||||
@@ -31,7 +31,7 @@ jest.mock('hooks/useDebounce', () => ({
|
||||
default: <T,>(value: T): T => value,
|
||||
}));
|
||||
|
||||
jest.mock('../QueryBuilderSearch/OptionRenderer', () => ({
|
||||
jest.mock('../OptionRenderer/OptionRenderer', () => ({
|
||||
__esModule: true,
|
||||
default: ({ value }: { value: string }): JSX.Element => <span>{value}</span>,
|
||||
}));
|
||||
|
||||
@@ -16,8 +16,8 @@ import { MetricAggregation } from 'types/api/v5/queryRange';
|
||||
import { ExtendedSelectOption } from 'types/common/select';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
import OptionRenderer from '../QueryBuilderSearch/OptionRenderer';
|
||||
import { selectStyle } from '../QueryBuilderSearchV2/config';
|
||||
import OptionRenderer from '../OptionRenderer/OptionRenderer';
|
||||
|
||||
import './MetricNameSelector.styles.scss';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { memo } from 'react';
|
||||
import { Select } from 'antd';
|
||||
|
||||
// ** Types
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
import { selectStyle } from '../QueryBuilderSearchV2/config';
|
||||
import { OperatorsSelectProps } from './OperatorsSelect.interfaces';
|
||||
|
||||
export const OperatorsSelect = memo(function OperatorsSelect({
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const TagContainer = styled(Badge).attrs({
|
||||
color: 'secondary',
|
||||
variant: 'outline',
|
||||
})`
|
||||
&&& {
|
||||
display: flex;
|
||||
font-weight: 300;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TagLabel = styled.span`
|
||||
font-weight: 400;
|
||||
`;
|
||||
|
||||
export const TagValue = styled.span`
|
||||
text-transform: capitalize;
|
||||
font-weight: 400;
|
||||
`;
|
||||
@@ -5,7 +5,7 @@ import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
import { getParsedAggregationOptionsForOrderBy } from 'utils/aggregationConverter';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
import { selectStyle } from '../QueryBuilderSearchV2/config';
|
||||
import { OrderByFilterProps } from './OrderByFilter.interfaces';
|
||||
import { useOrderByFilter } from './useOrderByFilter';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { parse } from 'papaparse';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { getRemoveOrderFromValue } from '../QueryBuilderSearch/utils';
|
||||
import { getRemoveOrderFromValue } from '../QueryBuilderSearchV2/utils';
|
||||
import { getUniqueOrderByValues, getValidOrderByResult } from '../utils';
|
||||
import { ORDERBY_FILTERS } from './config';
|
||||
import { SIGNOZ_VALUE } from './constants';
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { Zap } from '@signozhq/icons';
|
||||
|
||||
import { getOptionType } from './utils';
|
||||
|
||||
import './QueryBuilderSearch.styles.scss';
|
||||
|
||||
function OptionRendererForLogs({
|
||||
label,
|
||||
value,
|
||||
dataType,
|
||||
isIndexed,
|
||||
setDynamicPlaceholder,
|
||||
}: OptionRendererProps): JSX.Element {
|
||||
const optionType = getOptionType(label);
|
||||
|
||||
return (
|
||||
<span
|
||||
className="option"
|
||||
onMouseEnter={(): void => setDynamicPlaceholder(value)}
|
||||
onFocus={(): void => setDynamicPlaceholder(value)}
|
||||
>
|
||||
{optionType ? (
|
||||
<Tooltip title={value} placement="topLeft">
|
||||
<div className="logs-options-select">
|
||||
<section className="left-section">
|
||||
{isIndexed ? (
|
||||
<Zap size={12} fill={Color.BG_AMBER_500} />
|
||||
) : (
|
||||
<div className="dot" />
|
||||
)}
|
||||
<Typography.Text className="text value" truncate={1}>
|
||||
{value}
|
||||
</Typography.Text>
|
||||
</section>
|
||||
<section className="right-section">
|
||||
<div className="text tags data-type-tag">{dataType}</div>
|
||||
<div className={cx('text tags option-type-tag', optionType)}>
|
||||
<div className="dot" />
|
||||
{optionType}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title={label} placement="topLeft">
|
||||
<div className="without-option-type">
|
||||
<div className="dot" />
|
||||
<Typography.Text className="text" truncate={1}>
|
||||
{label}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface OptionRendererProps {
|
||||
label: string;
|
||||
value: string;
|
||||
dataType: string;
|
||||
isIndexed: boolean;
|
||||
setDynamicPlaceholder: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export default OptionRendererForLogs;
|
||||
@@ -1,298 +0,0 @@
|
||||
.query-builder-search-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logs-popup {
|
||||
&.hide-scroll {
|
||||
.rc-virtual-list-holder {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logs-explorer-popup {
|
||||
padding: 0px;
|
||||
.ant-select-item-group {
|
||||
padding: 12px 14px 8px 14px;
|
||||
color: var(--muted-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.show-all-filter-props {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 13px;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.left-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.text {
|
||||
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;
|
||||
}
|
||||
|
||||
.text:hover {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
.right-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
.keyboard-shortcut-slash {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2.286px;
|
||||
border-top: 1.143px solid var(--l1-border);
|
||||
border-right: 1.143px solid var(--l1-border);
|
||||
border-bottom: 2.286px solid var(--l1-border);
|
||||
border-left: 1.143px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.show-all-filter-props:hover {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l1-foreground) 4%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
.example-queries {
|
||||
cursor: default;
|
||||
.heading {
|
||||
padding: 12px 14px 8px 14px;
|
||||
color: var(--muted-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.query-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 0px 12px 12px 12px;
|
||||
cursor: pointer;
|
||||
|
||||
.example-query {
|
||||
display: flex;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-radius: 2px;
|
||||
background: var(--l3-background);
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: -0.07px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.example-query:hover {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-item-option-grouped {
|
||||
padding-inline-start: 0px;
|
||||
padding: 7px 13px;
|
||||
}
|
||||
|
||||
.keyboard-shortcuts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 0px 0px 4px 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
padding: 11px 16px;
|
||||
cursor: default;
|
||||
|
||||
.icons {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2.286px;
|
||||
border-top: 1.143px solid var(--l3-background);
|
||||
border-right: 1.143px solid var(--l3-background);
|
||||
border-bottom: 2.286px solid var(--l3-background);
|
||||
border-left: 1.143px solid var(--l3-background);
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
.keyboard-text {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.navigate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 12px;
|
||||
gap: 4px;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.update-query {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 12px;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.without-option-type {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
.dot {
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
.logs-options-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.text {
|
||||
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;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.left-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 90%;
|
||||
|
||||
.dot {
|
||||
background-color: var(--l3-background);
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.right-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.data-type-tag {
|
||||
background: color-mix(in srgb, var(--l1-foreground) 8%, transparent);
|
||||
}
|
||||
|
||||
.option-type-tag {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
padding: 0px 6px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.tag {
|
||||
border-radius: 50px;
|
||||
background: color-mix(in srgb, var(--bg-sienna-400) 10%, transparent);
|
||||
color: var(--bg-sienna-400);
|
||||
|
||||
.dot {
|
||||
background-color: var(--bg-sienna-400);
|
||||
}
|
||||
}
|
||||
|
||||
.resource {
|
||||
border-radius: 50px;
|
||||
background: color-mix(in srgb, var(--bg-sakura-400) 10%, transparent);
|
||||
color: var(--bg-sakura-400);
|
||||
|
||||
.dot {
|
||||
background-color: var(--bg-sakura-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-item-option-active {
|
||||
.logs-options-select {
|
||||
.left-section {
|
||||
.value {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.span-scope-selector {
|
||||
width: 160px;
|
||||
}
|
||||
@@ -1,558 +0,0 @@
|
||||
import {
|
||||
KeyboardEvent,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Button, Select, Spin, Tooltip } from 'antd';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
INFRA_LONG_TO_SHORT_OPERATOR_MAP,
|
||||
OPERATORS,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { getDataTypes } from 'container/LogDetailedView/utils';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import {
|
||||
useAutoComplete,
|
||||
WhereClauseConfig,
|
||||
} from 'hooks/queryBuilder/useAutoComplete';
|
||||
import { useFetchKeysAndValues } from 'hooks/queryBuilder/useFetchKeysAndValues';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { isEqual, isUndefined } from 'lodash-es';
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Command,
|
||||
CornerDownLeft,
|
||||
Filter,
|
||||
Slash,
|
||||
} 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 { DataSource } from 'types/common/queryBuilder';
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { selectStyle } from './config';
|
||||
import { PLACEHOLDER } from './constant';
|
||||
import ExampleQueriesRendererForLogs from './ExampleQueriesRendererForLogs';
|
||||
import OptionRenderer from './OptionRenderer';
|
||||
import OptionRendererForLogs from './OptionRendererForLogs';
|
||||
import { StyledCheckOutlined, TypographyText } from './style';
|
||||
import {
|
||||
convertExampleQueriesToOptions,
|
||||
getOperatorValue,
|
||||
getRemovePrefixFromKey,
|
||||
getTagToken,
|
||||
isExistsNotExistsOperator,
|
||||
isInNInOperator,
|
||||
} from './utils';
|
||||
|
||||
import './QueryBuilderSearch.styles.scss';
|
||||
|
||||
function getOperatorValueForContext(
|
||||
op: string,
|
||||
isInfraMonitoring?: boolean,
|
||||
): string {
|
||||
const mappedOp =
|
||||
isInfraMonitoring && INFRA_LONG_TO_SHORT_OPERATOR_MAP[op]
|
||||
? INFRA_LONG_TO_SHORT_OPERATOR_MAP[op]
|
||||
: op;
|
||||
return getOperatorValue(mappedOp);
|
||||
}
|
||||
|
||||
function QueryBuilderSearch({
|
||||
query,
|
||||
onChange,
|
||||
whereClauseConfig,
|
||||
className,
|
||||
placeholder,
|
||||
suffixIcon,
|
||||
isInfraMonitoring,
|
||||
isMetricsExplorer,
|
||||
disableNavigationShortcuts,
|
||||
entity,
|
||||
}: QueryBuilderSearchProps): JSX.Element {
|
||||
const getPopupContainer = useSelectPopupContainer();
|
||||
const { pathname } = useLocation();
|
||||
const isLogsExplorerPage = useMemo(
|
||||
() => pathname === ROUTES.LOGS_EXPLORER,
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const [isEditingTag, setIsEditingTag] = useState(false);
|
||||
|
||||
const {
|
||||
updateTag,
|
||||
handleClearTag,
|
||||
handleKeyDown,
|
||||
handleOnBlur,
|
||||
handleSearch,
|
||||
handleSelect,
|
||||
tags,
|
||||
options,
|
||||
searchValue,
|
||||
isMulti,
|
||||
isFetching,
|
||||
setSearchKey,
|
||||
setSearchValue,
|
||||
searchKey,
|
||||
key,
|
||||
exampleQueries,
|
||||
} = useAutoComplete(
|
||||
query,
|
||||
whereClauseConfig,
|
||||
isLogsExplorerPage,
|
||||
isInfraMonitoring,
|
||||
entity,
|
||||
isMetricsExplorer,
|
||||
);
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [showAllFilters, setShowAllFilters] = useState<boolean>(false);
|
||||
const [dynamicPlacholder, setDynamicPlaceholder] = useState<string>(
|
||||
placeholder || '',
|
||||
);
|
||||
const selectRef = useRef<BaseSelectRef>(null);
|
||||
|
||||
const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues(
|
||||
searchValue,
|
||||
query,
|
||||
searchKey,
|
||||
isLogsExplorerPage,
|
||||
isInfraMonitoring,
|
||||
entity,
|
||||
isMetricsExplorer,
|
||||
);
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
|
||||
const { handleRunQuery, currentQuery } = useQueryBuilder();
|
||||
|
||||
const toggleEditMode = useCallback(
|
||||
(value: boolean) => {
|
||||
// Editing mode is required only in infra monitoring or metrics explorer
|
||||
if (isInfraMonitoring || isMetricsExplorer) {
|
||||
setIsEditingTag(value);
|
||||
}
|
||||
},
|
||||
[isInfraMonitoring, isMetricsExplorer],
|
||||
);
|
||||
|
||||
const onTagRender = ({
|
||||
value,
|
||||
closable,
|
||||
onClose,
|
||||
}: CustomTagProps): ReactElement => {
|
||||
const { tagOperator } = getTagToken(value);
|
||||
const isInNin = isInNInOperator(tagOperator);
|
||||
const chipValue = isInNin
|
||||
? value?.trim()?.replace(/,\s*$/, '')
|
||||
: value?.trim();
|
||||
|
||||
const onCloseHandler = (): void => {
|
||||
onClose();
|
||||
// Editing is done after closing a tag
|
||||
toggleEditMode(false);
|
||||
handleSearch('');
|
||||
setSearchKey('');
|
||||
};
|
||||
|
||||
const tagEditHandler = (value: string): void => {
|
||||
updateTag(value);
|
||||
// Editing starts
|
||||
toggleEditMode(true);
|
||||
if (isInfraMonitoring || isMetricsExplorer) {
|
||||
setSearchValue(value);
|
||||
} else {
|
||||
handleSearch(value);
|
||||
}
|
||||
};
|
||||
|
||||
const isDisabled = !!searchValue;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
color="vanilla"
|
||||
closable={!searchValue && closable}
|
||||
onClose={(e): void => {
|
||||
e.preventDefault();
|
||||
onCloseHandler();
|
||||
}}
|
||||
>
|
||||
<Tooltip title={chipValue}>
|
||||
<TypographyText
|
||||
$isInNin={isInNin}
|
||||
$isEnabled={!!searchValue}
|
||||
onClick={(): void => {
|
||||
if (!isDisabled) {
|
||||
tagEditHandler(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{chipValue}
|
||||
</TypographyText>
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const onChangeHandler = (value: string[]): void => {
|
||||
if (!isMulti) {
|
||||
handleSearch(value[value.length - 1]);
|
||||
}
|
||||
};
|
||||
|
||||
const onInputKeyDownHandler = (event: KeyboardEvent<Element>): void => {
|
||||
if (isMulti || event.key === 'Backspace') {
|
||||
handleKeyDown(event);
|
||||
}
|
||||
if (isExistsNotExistsOperator(searchValue)) {
|
||||
handleKeyDown(event);
|
||||
}
|
||||
|
||||
// Editing is done after enter key press
|
||||
if (event.key === 'Enter') {
|
||||
toggleEditMode(false);
|
||||
}
|
||||
|
||||
if (
|
||||
!disableNavigationShortcuts &&
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key === 'Enter'
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleRunQuery();
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
if (
|
||||
!disableNavigationShortcuts &&
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key === '/'
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setShowAllFilters((prev) => !prev);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeselect = useCallback(
|
||||
(deselectedItem: string) => {
|
||||
handleClearTag(deselectedItem);
|
||||
handleRemoveSourceKey(deselectedItem);
|
||||
},
|
||||
[handleClearTag, handleRemoveSourceKey],
|
||||
);
|
||||
|
||||
const isMetricsDataSource = useMemo(
|
||||
() =>
|
||||
query.dataSource === DataSource.METRICS &&
|
||||
!isInfraMonitoring &&
|
||||
!isMetricsExplorer,
|
||||
[query.dataSource, isInfraMonitoring, isMetricsExplorer],
|
||||
);
|
||||
|
||||
const fetchValueDataType = (value: unknown, operator: string): DataTypes => {
|
||||
if (operator === OPERATORS.HAS || operator === OPERATORS.NHAS) {
|
||||
return getDataTypes([value]);
|
||||
}
|
||||
|
||||
return DataTypes.EMPTY;
|
||||
};
|
||||
|
||||
const queryTags = useMemo(() => {
|
||||
if (!query.aggregateAttribute?.key && isMetricsDataSource) {
|
||||
return [];
|
||||
}
|
||||
return tags;
|
||||
}, [isMetricsDataSource, query.aggregateAttribute?.key, tags]);
|
||||
|
||||
useEffect(() => {
|
||||
const initialTagFilters: TagFilter = { items: [], op: 'AND' };
|
||||
const initialSourceKeys = query.filters?.items?.map(
|
||||
(item) => item.key as BaseAutocompleteData,
|
||||
);
|
||||
|
||||
initialTagFilters.items = tags.map((tag) => {
|
||||
const { tagKey, tagOperator, tagValue } = getTagToken(tag);
|
||||
|
||||
const filterAttribute = [
|
||||
...(initialSourceKeys || []),
|
||||
...(sourceKeys || []),
|
||||
].find((key) => key?.key === getRemovePrefixFromKey(tagKey));
|
||||
|
||||
const computedTagValue =
|
||||
tagValue && Array.isArray(tagValue) && tagValue[tagValue.length - 1] === ''
|
||||
? tagValue?.slice(0, -1)
|
||||
: (tagValue ?? '');
|
||||
|
||||
return {
|
||||
id: uuid().slice(0, 8),
|
||||
key: filterAttribute ?? {
|
||||
key: tagKey,
|
||||
dataType: fetchValueDataType(computedTagValue, tagOperator),
|
||||
type: '',
|
||||
},
|
||||
op: getOperatorValueForContext(tagOperator, isInfraMonitoring),
|
||||
value: computedTagValue,
|
||||
};
|
||||
});
|
||||
|
||||
// If in infra monitoring or metrics explorer, only run the onChange query when editing is finsished.
|
||||
if (isInfraMonitoring || isMetricsExplorer) {
|
||||
if (!isEditingTag) {
|
||||
onChange(initialTagFilters);
|
||||
}
|
||||
} else {
|
||||
onChange(initialTagFilters);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sourceKeys]);
|
||||
|
||||
const isLastQuery = useMemo(
|
||||
() =>
|
||||
isEqual(
|
||||
currentQuery.builder.queryData[currentQuery.builder.queryData.length - 1],
|
||||
query,
|
||||
),
|
||||
[currentQuery, query],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLastQuery && !disableNavigationShortcuts) {
|
||||
registerShortcut(LogsExplorerShortcuts.FocusTheSearchBar, () => {
|
||||
// set timeout is needed here else the select treats the hotkey as input value
|
||||
setTimeout(() => {
|
||||
selectRef.current?.focus();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
return (): void =>
|
||||
deregisterShortcut(LogsExplorerShortcuts.FocusTheSearchBar);
|
||||
}, [
|
||||
deregisterShortcut,
|
||||
disableNavigationShortcuts,
|
||||
isLastQuery,
|
||||
registerShortcut,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setDynamicPlaceholder(placeholder || '');
|
||||
}
|
||||
}, [isOpen, placeholder]);
|
||||
|
||||
const userOs = getUserOperatingSystem();
|
||||
|
||||
// conditional changes here to use a seperate component to render the example queries based on the option group label
|
||||
const customRendererForLogsExplorer = options.map((option) => (
|
||||
<Select.Option
|
||||
key={`${option.label}-${option.type || ''}-${option.dataType || ''}`}
|
||||
value={option.value}
|
||||
>
|
||||
<OptionRendererForLogs
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
dataType={option.dataType || ''}
|
||||
isIndexed={option.isIndexed || false}
|
||||
setDynamicPlaceholder={setDynamicPlaceholder}
|
||||
/>
|
||||
{option.selected && <StyledCheckOutlined />}
|
||||
</Select.Option>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="query-builder-search-container">
|
||||
<Select
|
||||
data-testid={'qb-search-select'}
|
||||
ref={selectRef}
|
||||
getPopupContainer={getPopupContainer}
|
||||
transitionName=""
|
||||
choiceTransitionName=""
|
||||
virtual={false}
|
||||
showSearch
|
||||
tagRender={onTagRender}
|
||||
filterOption={false}
|
||||
open={isOpen}
|
||||
onDropdownVisibleChange={setIsOpen}
|
||||
autoClearSearchValue={false}
|
||||
mode="multiple"
|
||||
placeholder={dynamicPlacholder}
|
||||
value={queryTags}
|
||||
searchValue={searchValue}
|
||||
className={cx(
|
||||
className,
|
||||
isLogsExplorerPage ? 'logs-popup' : '',
|
||||
!showAllFilters && options.length > 3 && !key ? 'hide-scroll' : '',
|
||||
)}
|
||||
rootClassName="query-builder-search"
|
||||
disabled={isMetricsDataSource && !query.aggregateAttribute?.key}
|
||||
style={selectStyle}
|
||||
onSearch={handleSearch}
|
||||
onChange={onChangeHandler}
|
||||
onSelect={handleSelect}
|
||||
onDeselect={handleDeselect}
|
||||
onInputKeyDown={onInputKeyDownHandler}
|
||||
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||
suffixIcon={
|
||||
!isUndefined(suffixIcon) ? (
|
||||
suffixIcon
|
||||
) : isOpen ? (
|
||||
<ChevronUp size={14} />
|
||||
) : (
|
||||
<ChevronDown size={14} />
|
||||
)
|
||||
}
|
||||
showAction={['focus']}
|
||||
onBlur={(e: React.FocusEvent<HTMLInputElement>): void => {
|
||||
handleOnBlur(e);
|
||||
// Editing is done after tapping out of the input
|
||||
toggleEditMode(false);
|
||||
}}
|
||||
popupClassName={isLogsExplorerPage ? 'logs-explorer-popup' : ''}
|
||||
dropdownRender={(menu): ReactElement => (
|
||||
<div>
|
||||
{!searchKey && isLogsExplorerPage && (
|
||||
<div className="ant-select-item-group ">Suggested Filters</div>
|
||||
)}
|
||||
{menu}
|
||||
{isLogsExplorerPage && (
|
||||
<div>
|
||||
{!searchKey && tags.length === 0 && (
|
||||
<div className="example-queries">
|
||||
<div className="heading"> Example Queries </div>
|
||||
<div className="query-container">
|
||||
{convertExampleQueriesToOptions(exampleQueries).map((query) => (
|
||||
<ExampleQueriesRendererForLogs
|
||||
key={query.label}
|
||||
label={query.label}
|
||||
value={query.value}
|
||||
handleAddTag={onChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!key && !isFetching && !showAllFilters && options.length > 3 && (
|
||||
<Button
|
||||
type="text"
|
||||
className="show-all-filter-props"
|
||||
onClick={(): void => {
|
||||
setShowAllFilters(true);
|
||||
// when clicking on the button the search bar looses the focus
|
||||
selectRef?.current?.focus();
|
||||
}}
|
||||
>
|
||||
<div className="content">
|
||||
<section className="left-section">
|
||||
<Filter size={14} />
|
||||
<Typography.Text className="text">
|
||||
Show all filters properties
|
||||
</Typography.Text>
|
||||
</section>
|
||||
<section className="right-section">
|
||||
{userOs === UserOperatingSystem.MACOS ? (
|
||||
<Command size={14} className="keyboard-shortcut-slash" />
|
||||
) : (
|
||||
<ChevronUp size={14} className="keyboard-shortcut-slash" />
|
||||
)}
|
||||
+
|
||||
<Slash size={14} className="keyboard-shortcut-slash" />
|
||||
</section>
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
<div className="keyboard-shortcuts">
|
||||
<section className="navigate">
|
||||
<ArrowDown size={10} className="icons" />
|
||||
<ArrowUp size={10} className="icons" />
|
||||
<span className="keyboard-text">to navigate</span>
|
||||
</section>
|
||||
<section className="update-query">
|
||||
<CornerDownLeft size={10} className="icons" />
|
||||
<span className="keyboard-text">to update query</span>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{isLogsExplorerPage
|
||||
? customRendererForLogsExplorer
|
||||
: options.map((option) => (
|
||||
<Select.Option
|
||||
key={`${option.label}-${option.type || ''}-${option.dataType || ''}`}
|
||||
value={option.value}
|
||||
>
|
||||
<OptionRenderer
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
dataType={option.dataType || ''}
|
||||
type={option.type || ''}
|
||||
/>
|
||||
{option.selected && <StyledCheckOutlined />}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface QueryBuilderSearchProps {
|
||||
query: IBuilderQuery;
|
||||
onChange: (value: TagFilter) => void;
|
||||
whereClauseConfig?: WhereClauseConfig;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
suffixIcon?: React.ReactNode;
|
||||
isInfraMonitoring?: boolean;
|
||||
disableNavigationShortcuts?: boolean;
|
||||
// TODO: Remove the dependency of InfraMonitoring from this code
|
||||
entity?: InfraMonitoringEntity | null;
|
||||
isMetricsExplorer?: boolean;
|
||||
}
|
||||
|
||||
QueryBuilderSearch.defaultProps = {
|
||||
whereClauseConfig: undefined,
|
||||
className: '',
|
||||
placeholder: PLACEHOLDER,
|
||||
suffixIcon: undefined,
|
||||
isInfraMonitoring: false,
|
||||
disableNavigationShortcuts: false,
|
||||
entity: null,
|
||||
isMetricsExplorer: false,
|
||||
};
|
||||
|
||||
export interface CustomTagProps {
|
||||
label: ReactNode;
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
onClose: () => void;
|
||||
closable: boolean;
|
||||
}
|
||||
|
||||
export default QueryBuilderSearch;
|
||||
@@ -1,7 +1,5 @@
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import './QueryBuilderSearch.styles.scss';
|
||||
|
||||
function ExampleQueriesRendererForLogs({
|
||||
label,
|
||||
value,
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
|
||||
import ExampleQueriesRendererForLogs from '../QueryBuilderSearch/ExampleQueriesRendererForLogs';
|
||||
import { convertExampleQueriesToOptions } from '../QueryBuilderSearch/utils';
|
||||
import ExampleQueriesRendererForLogs from './ExampleQueriesRendererForLogs';
|
||||
import { convertExampleQueriesToOptions } from './utils';
|
||||
import { ITag, Option } from './QueryBuilderSearchV2';
|
||||
|
||||
import './QueryBuilderSearchV2.styles.scss';
|
||||
|
||||
@@ -19,10 +19,10 @@ import {
|
||||
QUERY_BUILDER_SEARCH_VALUES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
import type { WhereClauseConfig } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
|
||||
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
|
||||
@@ -53,16 +53,16 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
import { PLACEHOLDER } from '../QueryBuilderSearch/constant';
|
||||
import { TypographyText } from '../QueryBuilderSearch/style';
|
||||
import { selectStyle } from './config';
|
||||
import { PLACEHOLDER } from './constant';
|
||||
import { TypographyText } from './style';
|
||||
import {
|
||||
checkCommaInValue,
|
||||
getOperatorFromValue,
|
||||
getOperatorValue,
|
||||
getTagToken,
|
||||
isInNInOperator,
|
||||
} from '../QueryBuilderSearch/utils';
|
||||
} from './utils';
|
||||
import { filterByOperatorConfig } from '../utils';
|
||||
import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown';
|
||||
import SpanScopeSelector from './SpanScopeSelector';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { isEmpty, isObject } from 'lodash-es';
|
||||
import { Check, Zap } from '@signozhq/icons';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { getTagToken } from '../QueryBuilderSearch/utils';
|
||||
import { getTagToken } from './utils';
|
||||
import { DropdownState } from './QueryBuilderSearchV2';
|
||||
|
||||
import './Suggestions.styles.scss';
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Check } from '@signozhq/icons';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const TypographyText = styled.span<{
|
||||
@@ -17,27 +15,3 @@ export const TypographyText = styled.span<{
|
||||
padding-left: 8px;
|
||||
${({ $disabled }): string => ($disabled ? 'opacity: 0.6' : '')}
|
||||
`;
|
||||
|
||||
export const StyledCheckOutlined = styled(Check)`
|
||||
float: right;
|
||||
`;
|
||||
|
||||
export const TagContainer = styled(Badge).attrs({
|
||||
color: 'secondary',
|
||||
variant: 'outline',
|
||||
})`
|
||||
&&& {
|
||||
display: flex;
|
||||
font-weight: 300;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TagLabel = styled.span`
|
||||
font-weight: 400;
|
||||
`;
|
||||
|
||||
export const TagValue = styled.span`
|
||||
text-transform: capitalize;
|
||||
font-weight: 400;
|
||||
`;
|
||||
@@ -1,8 +1,7 @@
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { queryFilterTags } from 'hooks/queryBuilder/useTag';
|
||||
import { parse } from 'papaparse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import type { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { orderByValueDelimiter } from '../OrderByFilter/utils';
|
||||
|
||||
@@ -191,18 +190,6 @@ export function getRemoveOrderFromValue(tag: string): string {
|
||||
return tag;
|
||||
}
|
||||
|
||||
export function getOptionType(label: string): MetricsType | undefined {
|
||||
let optionType;
|
||||
|
||||
if (label.startsWith('tag_')) {
|
||||
optionType = MetricsType.Tag;
|
||||
} else if (label.startsWith('resource_')) {
|
||||
optionType = MetricsType.Resource;
|
||||
}
|
||||
|
||||
return optionType;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param exampleQueries the example queries based on recommendation engine
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AttributeValuesMap } from 'components/ClientSideQBSearch/ClientSideQBSearch';
|
||||
import { OperatorConfigKeys, OPERATORS_CONFIG } from 'constants/queryBuilder';
|
||||
import { HAVING_FILTER_REGEXP } from 'constants/regExp';
|
||||
import { IOption } from 'hooks/useResourceAttribute/types';
|
||||
@@ -11,7 +10,7 @@ import {
|
||||
orderByValueDelimiter,
|
||||
splitOrderByFromString,
|
||||
} from './OrderByFilter/utils';
|
||||
import { getRemoveOrderFromValue } from './QueryBuilderSearch/utils';
|
||||
import { getRemoveOrderFromValue } from './QueryBuilderSearchV2/utils';
|
||||
|
||||
export const handleKeyDownLimitFilter: React.KeyboardEventHandler<
|
||||
HTMLInputElement
|
||||
@@ -101,23 +100,6 @@ export const getValidOrderByResult = (result: IOption[]): IOption[] =>
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
export const transformKeyValuesToAttributeValuesMap = (
|
||||
attributeValuesMap: Record<string, string[] | number[] | boolean[]>,
|
||||
): AttributeValuesMap =>
|
||||
Object.fromEntries(
|
||||
Object.entries(attributeValuesMap || {}).map(([key, values]) => [
|
||||
key,
|
||||
{
|
||||
stringAttributeValues:
|
||||
typeof values[0] === 'string' ? (values as string[]) : [],
|
||||
numberAttributeValues:
|
||||
typeof values[0] === 'number' ? (values as number[]) : [],
|
||||
boolAttributeValues:
|
||||
typeof values[0] === 'boolean' ? (values as boolean[]) : [],
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
export const filterByOperatorConfig = (
|
||||
options: IOption[],
|
||||
key?: OperatorConfigKeys,
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
formatValueForExpression,
|
||||
removeKeysFromExpression,
|
||||
} from 'components/QueryBuilderV2/utils';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { IQueryPair } from 'types/antlrQueryTypes';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { isQuoted, unquote } from 'utils/stringUtils';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { QueryParams } from 'constants/query';
|
||||
import { OPERATORS, QueryBuilderKeys } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
import { KeyboardEvent, useCallback, useState } from 'react';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import type { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import {
|
||||
getRemovePrefixFromKey,
|
||||
getTagToken,
|
||||
replaceStringWithMaxLength,
|
||||
tagRegexp,
|
||||
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { Option } from 'container/QueryBuilder/type';
|
||||
import { parse } from 'papaparse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { useFetchKeysAndValues } from './useFetchKeysAndValues';
|
||||
import { useOptions, WHERE_CLAUSE_CUSTOM_SUFFIX } from './useOptions';
|
||||
import { useSetCurrentKeyAndOperator } from './useSetCurrentKeyAndOperator';
|
||||
import { useTag } from './useTag';
|
||||
import { useTagValidation } from './useTagValidation';
|
||||
|
||||
export type WhereClauseConfig = {
|
||||
customKey: string;
|
||||
customOp: string;
|
||||
};
|
||||
|
||||
export const useAutoComplete = (
|
||||
query: IBuilderQuery,
|
||||
whereClauseConfig?: WhereClauseConfig,
|
||||
shouldUseSuggestions?: boolean,
|
||||
isInfraMonitoring?: boolean,
|
||||
entity?: InfraMonitoringEntity | null,
|
||||
isMetricsExplorer?: boolean,
|
||||
): IAutoComplete => {
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [searchKey, setSearchKey] = useState<string>('');
|
||||
|
||||
const { keys, results, isFetching, exampleQueries } = useFetchKeysAndValues(
|
||||
searchValue,
|
||||
query,
|
||||
searchKey,
|
||||
shouldUseSuggestions,
|
||||
isInfraMonitoring,
|
||||
entity,
|
||||
isMetricsExplorer,
|
||||
);
|
||||
|
||||
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
|
||||
|
||||
const handleSearch = (value: string): void => {
|
||||
const prefixFreeValue = getRemovePrefixFromKey(getTagToken(value).tagKey);
|
||||
setSearchValue(value);
|
||||
setSearchKey(prefixFreeValue);
|
||||
};
|
||||
|
||||
const { isValidTag, isExist, isValidOperator, isMulti } = useTagValidation(
|
||||
operator,
|
||||
result,
|
||||
);
|
||||
|
||||
const { handleAddTag, handleClearTag, tags, updateTag } = useTag(
|
||||
isValidTag,
|
||||
handleSearch,
|
||||
query,
|
||||
setSearchKey,
|
||||
whereClauseConfig,
|
||||
isInfraMonitoring,
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(value: string): void => {
|
||||
if (isMulti) {
|
||||
setSearchValue((prev: string) => {
|
||||
const matches = prev?.matchAll(tagRegexp);
|
||||
const [match] = matches ? Array.from(matches) : [];
|
||||
const [, , , matchTagValue] = match;
|
||||
const data = parse(matchTagValue).data.flat();
|
||||
return replaceStringWithMaxLength(prev, data as string[], value);
|
||||
});
|
||||
}
|
||||
if (!isMulti) {
|
||||
handleAddTag(value);
|
||||
}
|
||||
},
|
||||
[handleAddTag, isMulti],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent): void => {
|
||||
if (
|
||||
event.key === ' ' &&
|
||||
(searchValue.endsWith(' ') || searchValue.length === 0)
|
||||
) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && searchValue && isValidTag) {
|
||||
if (isMulti) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
event.preventDefault();
|
||||
handleAddTag(searchValue);
|
||||
}
|
||||
|
||||
if (event.key === 'Backspace' && !searchValue) {
|
||||
event.stopPropagation();
|
||||
const last = tags[tags.length - 1];
|
||||
handleClearTag(last);
|
||||
}
|
||||
},
|
||||
[handleAddTag, handleClearTag, isMulti, isValidTag, searchValue, tags],
|
||||
);
|
||||
|
||||
const handleOnBlur = (event: React.FocusEvent<HTMLInputElement>): void => {
|
||||
event.preventDefault();
|
||||
if (searchValue) {
|
||||
if (
|
||||
key &&
|
||||
!operator &&
|
||||
whereClauseConfig?.customKey === 'body' &&
|
||||
whereClauseConfig.customOp === OPERATORS.CONTAINS
|
||||
) {
|
||||
const value = `${searchValue}${WHERE_CLAUSE_CUSTOM_SUFFIX}`;
|
||||
handleAddTag(value);
|
||||
return;
|
||||
}
|
||||
handleAddTag(searchValue);
|
||||
}
|
||||
};
|
||||
|
||||
const options = useOptions(
|
||||
key,
|
||||
keys,
|
||||
operator,
|
||||
searchValue,
|
||||
isMulti,
|
||||
isValidOperator,
|
||||
isExist,
|
||||
results,
|
||||
result,
|
||||
isFetching,
|
||||
whereClauseConfig,
|
||||
isInfraMonitoring,
|
||||
);
|
||||
|
||||
return {
|
||||
updateTag,
|
||||
handleSearch,
|
||||
handleClearTag,
|
||||
handleSelect,
|
||||
handleKeyDown,
|
||||
handleOnBlur,
|
||||
options,
|
||||
tags,
|
||||
searchValue,
|
||||
isMulti,
|
||||
isFetching,
|
||||
setSearchKey,
|
||||
setSearchValue,
|
||||
searchKey,
|
||||
key,
|
||||
exampleQueries,
|
||||
};
|
||||
};
|
||||
|
||||
interface IAutoComplete {
|
||||
updateTag: (value: string) => void;
|
||||
handleSearch: (value: string) => void;
|
||||
handleClearTag: (value: string) => void;
|
||||
handleSelect: (value: string) => void;
|
||||
handleKeyDown: (event: React.KeyboardEvent) => void;
|
||||
handleOnBlur: (event: React.FocusEvent<HTMLInputElement>) => void;
|
||||
options: Option[];
|
||||
tags: string[];
|
||||
searchValue: string;
|
||||
isMulti: boolean;
|
||||
isFetching: boolean;
|
||||
setSearchKey: (value: string) => void;
|
||||
setSearchValue: (value: string) => void;
|
||||
searchKey: string;
|
||||
key: string;
|
||||
exampleQueries: TagFilter[];
|
||||
isInfraMonitoring?: boolean;
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
|
||||
import { DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY } from 'constants/queryBuilder';
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
InfraMonitoringEntity,
|
||||
} from 'container/InfraMonitoringK8sV2/constants';
|
||||
import {
|
||||
getRemovePrefixFromKey,
|
||||
getTagToken,
|
||||
isInNInOperator,
|
||||
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import useDebounceValue from 'hooks/useDebounce';
|
||||
import { cloneDeep, isEqual, uniqWith, unset } from 'lodash-es';
|
||||
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { useGetAggregateKeys } from './useGetAggregateKeys';
|
||||
import { useGetAttributeSuggestions } from './useGetAttributeSuggestions';
|
||||
|
||||
type IuseFetchKeysAndValues = {
|
||||
keys: BaseAutocompleteData[];
|
||||
results: string[];
|
||||
isFetching: boolean;
|
||||
sourceKeys: BaseAutocompleteData[];
|
||||
handleRemoveSourceKey: (newSourceKey: string) => void;
|
||||
exampleQueries: TagFilter[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook to fetch attribute keys and values from an API
|
||||
* @param searchValue - the search query value
|
||||
* @param query - an object containing data for the query
|
||||
* @returns an object containing the fetched attribute keys, results, and the status of the fetch
|
||||
*/
|
||||
|
||||
export const useFetchKeysAndValues = (
|
||||
searchValue: string,
|
||||
query: IBuilderQuery,
|
||||
searchKey: string,
|
||||
shouldUseSuggestions?: boolean,
|
||||
isInfraMonitoring?: boolean,
|
||||
entity?: InfraMonitoringEntity | null,
|
||||
isMetricsExplorer?: boolean,
|
||||
): IuseFetchKeysAndValues => {
|
||||
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
|
||||
const [exampleQueries, setExampleQueries] = useState<TagFilter[]>([]);
|
||||
const [sourceKeys, setSourceKeys] = useState<BaseAutocompleteData[]>([]);
|
||||
const [results, setResults] = useState<string[]>([]);
|
||||
const [isAggregateFetching, setAggregateFetching] = useState<boolean>(false);
|
||||
|
||||
const memoizedSearchParams = useMemo(
|
||||
() => [
|
||||
searchKey,
|
||||
query.dataSource,
|
||||
query.aggregateOperator,
|
||||
query.aggregateAttribute?.key,
|
||||
],
|
||||
[
|
||||
searchKey,
|
||||
query.dataSource,
|
||||
query.aggregateOperator,
|
||||
query.aggregateAttribute?.key,
|
||||
],
|
||||
);
|
||||
|
||||
const searchParams = useDebounceValue(memoizedSearchParams, DEBOUNCE_DELAY);
|
||||
|
||||
const queryFiltersWithoutId = useMemo(
|
||||
() => ({
|
||||
...query.filters,
|
||||
items: query.filters?.items?.map((item) => {
|
||||
const filterWithoutId = cloneDeep(item);
|
||||
unset(filterWithoutId, 'id');
|
||||
return filterWithoutId;
|
||||
}),
|
||||
}),
|
||||
[query.filters],
|
||||
);
|
||||
|
||||
const memoizedSuggestionsParams = useMemo(
|
||||
() => [searchKey, query.dataSource, queryFiltersWithoutId],
|
||||
[query.dataSource, queryFiltersWithoutId, searchKey],
|
||||
);
|
||||
|
||||
const suggestionsParams = useDebounceValue(
|
||||
memoizedSuggestionsParams,
|
||||
DEBOUNCE_DELAY,
|
||||
);
|
||||
|
||||
const isQueryEnabled = useMemo(
|
||||
() =>
|
||||
query.dataSource === DataSource.METRICS &&
|
||||
!isInfraMonitoring &&
|
||||
!isMetricsExplorer
|
||||
? !!query.dataSource && !!query.aggregateAttribute?.dataType
|
||||
: true,
|
||||
[
|
||||
isInfraMonitoring,
|
||||
isMetricsExplorer,
|
||||
query.aggregateAttribute?.dataType,
|
||||
query.dataSource,
|
||||
],
|
||||
);
|
||||
|
||||
const { data, isFetching, status } = useGetAggregateKeys(
|
||||
{
|
||||
searchText: searchKey,
|
||||
dataSource: query.dataSource,
|
||||
aggregateOperator: query.aggregateOperator || '',
|
||||
aggregateAttribute:
|
||||
isInfraMonitoring && entity
|
||||
? GetK8sEntityToAggregateAttribute(entity)
|
||||
: query.aggregateAttribute?.key || '',
|
||||
tagType: query.aggregateAttribute?.type ?? null,
|
||||
},
|
||||
{
|
||||
queryKey: [searchParams],
|
||||
enabled: isMetricsExplorer ? false : isQueryEnabled && !shouldUseSuggestions,
|
||||
},
|
||||
isInfraMonitoring, // isInfraMonitoring
|
||||
entity, // infraMonitoringEntity
|
||||
);
|
||||
|
||||
const {
|
||||
data: suggestionsData,
|
||||
isFetching: isFetchingSuggestions,
|
||||
status: fetchingSuggestionsStatus,
|
||||
} = useGetAttributeSuggestions(
|
||||
{
|
||||
searchText: searchKey,
|
||||
dataSource: query.dataSource,
|
||||
filters: query.filters || { items: [], op: 'AND' },
|
||||
},
|
||||
{
|
||||
queryKey: [suggestionsParams],
|
||||
enabled: isQueryEnabled && shouldUseSuggestions,
|
||||
},
|
||||
);
|
||||
|
||||
function isAttributeValuesResponse(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
payload: any,
|
||||
): payload is IAttributeValuesResponse {
|
||||
return (
|
||||
payload &&
|
||||
(Array.isArray(payload.stringAttributeValues) ||
|
||||
payload.stringAttributeValues === null ||
|
||||
Array.isArray(payload.numberAttributeValues) ||
|
||||
payload.numberAttributeValues === null ||
|
||||
Array.isArray(payload.boolAttributeValues) ||
|
||||
payload.boolAttributeValues === null)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the options to be displayed based on the selected value
|
||||
* @param value - the selected value
|
||||
* @param query - an object containing data for the query
|
||||
*/
|
||||
const handleFetchOption = async (
|
||||
value: string,
|
||||
query: IBuilderQuery,
|
||||
keys: BaseAutocompleteData[],
|
||||
): Promise<void> => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const { tagKey, tagOperator, tagValue } = getTagToken(value);
|
||||
const filterAttributeKey = keys.find(
|
||||
(item) => item.key === getRemovePrefixFromKey(tagKey),
|
||||
);
|
||||
setResults([]);
|
||||
|
||||
if (!tagKey || !tagOperator) {
|
||||
return;
|
||||
}
|
||||
setAggregateFetching(true);
|
||||
|
||||
try {
|
||||
let payload;
|
||||
if (isInfraMonitoring && entity) {
|
||||
const response = await getAttributesValues({
|
||||
aggregateOperator: 'noop',
|
||||
dataSource: query.dataSource,
|
||||
aggregateAttribute:
|
||||
GetK8sEntityToAggregateAttribute(entity) ||
|
||||
query.aggregateAttribute?.key ||
|
||||
'',
|
||||
attributeKey: filterAttributeKey?.key ?? tagKey,
|
||||
filterAttributeKeyDataType:
|
||||
filterAttributeKey?.dataType ?? DataTypes.EMPTY,
|
||||
tagType: filterAttributeKey?.type ?? '',
|
||||
searchText: isInNInOperator(tagOperator)
|
||||
? (tagValue[tagValue.length - 1]?.toString() ?? '')
|
||||
: (tagValue?.toString() ?? ''),
|
||||
});
|
||||
payload = response.payload;
|
||||
} else {
|
||||
const response = await getAttributesValues({
|
||||
aggregateOperator: query.aggregateOperator || '',
|
||||
dataSource: query.dataSource,
|
||||
aggregateAttribute: query.aggregateAttribute?.key || '',
|
||||
attributeKey: filterAttributeKey?.key ?? tagKey,
|
||||
filterAttributeKeyDataType:
|
||||
filterAttributeKey?.dataType ?? DataTypes.EMPTY,
|
||||
tagType: filterAttributeKey?.type ?? '',
|
||||
searchText: isInNInOperator(tagOperator)
|
||||
? (tagValue[tagValue.length - 1]?.toString() ?? '')
|
||||
: (tagValue?.toString() ?? ''),
|
||||
});
|
||||
payload = response.payload;
|
||||
}
|
||||
|
||||
if (payload && isAttributeValuesResponse(payload)) {
|
||||
const dataType = filterAttributeKey?.dataType ?? DataTypes.String;
|
||||
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
|
||||
setResults(key ? payload[key] || [] : []);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setAggregateFetching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveSourceKey = useCallback((sourceKey: string) => {
|
||||
setSourceKeys((prevState) =>
|
||||
prevState.filter((item) => item.key !== sourceKey),
|
||||
);
|
||||
}, []);
|
||||
|
||||
// creates a ref to the fetch function so that it doesn't change on every render
|
||||
const clearFetcher = useRef(handleFetchOption).current;
|
||||
|
||||
// debounces the fetch function to avoid excessive API calls
|
||||
useDebounce(() => clearFetcher(searchValue, query, keys), 750, [
|
||||
clearFetcher,
|
||||
searchValue,
|
||||
query,
|
||||
keys,
|
||||
]);
|
||||
|
||||
// update the fetched keys when the fetch status changes
|
||||
useEffect(() => {
|
||||
if (status === 'success' && data?.payload?.attributeKeys) {
|
||||
setKeys(data.payload.attributeKeys);
|
||||
setSourceKeys((prevState) =>
|
||||
uniqWith([...(data.payload.attributeKeys ?? []), ...prevState], isEqual),
|
||||
);
|
||||
} else {
|
||||
setKeys([]);
|
||||
}
|
||||
}, [data?.payload?.attributeKeys, status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
fetchingSuggestionsStatus === 'success' &&
|
||||
suggestionsData?.payload?.attributes
|
||||
) {
|
||||
if (!isInfraMonitoring) {
|
||||
setKeys(suggestionsData.payload.attributes);
|
||||
setSourceKeys((prevState) =>
|
||||
uniqWith(
|
||||
[...(suggestionsData.payload.attributes ?? []), ...prevState],
|
||||
isEqual,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setKeys([]);
|
||||
}
|
||||
if (
|
||||
fetchingSuggestionsStatus === 'success' &&
|
||||
suggestionsData?.payload?.example_queries
|
||||
) {
|
||||
setExampleQueries(suggestionsData.payload.example_queries);
|
||||
} else {
|
||||
setExampleQueries([]);
|
||||
}
|
||||
}, [
|
||||
suggestionsData?.payload?.attributes,
|
||||
fetchingSuggestionsStatus,
|
||||
suggestionsData?.payload?.example_queries,
|
||||
isInfraMonitoring,
|
||||
]);
|
||||
|
||||
return {
|
||||
keys,
|
||||
results,
|
||||
isFetching: isFetching || isAggregateFetching || isFetchingSuggestions,
|
||||
sourceKeys,
|
||||
handleRemoveSourceKey,
|
||||
exampleQueries,
|
||||
};
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
OPERATORS,
|
||||
QUERY_BUILDER_OPERATORS_BY_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { getRemovePrefixFromKey } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
type IOperators =
|
||||
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.universal
|
||||
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.string
|
||||
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.bool
|
||||
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.int64
|
||||
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.float64;
|
||||
|
||||
export const useOperators = (
|
||||
key: string,
|
||||
keys: BaseAutocompleteData[],
|
||||
): IOperators =>
|
||||
useMemo(() => {
|
||||
const currentKey = keys?.find((el) => el.key === getRemovePrefixFromKey(key));
|
||||
const strippedKey = key.split(' ')[0];
|
||||
|
||||
return currentKey?.dataType
|
||||
? QUERY_BUILDER_OPERATORS_BY_TYPES[
|
||||
currentKey.dataType as keyof typeof QUERY_BUILDER_OPERATORS_BY_TYPES
|
||||
]
|
||||
: strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')
|
||||
? [OPERATORS.HAS, OPERATORS.NHAS]
|
||||
: QUERY_BUILDER_OPERATORS_BY_TYPES.universal;
|
||||
}, [keys, key]);
|
||||
@@ -1,220 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { INFRA_SHORT_TO_LONG_OPERATOR_MAP } from 'constants/queryBuilder';
|
||||
import {
|
||||
checkCommaInValue,
|
||||
getTagToken,
|
||||
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { Option } from 'container/QueryBuilder/type';
|
||||
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { WhereClauseConfig } from './useAutoComplete';
|
||||
import { useOperators } from './useOperators';
|
||||
|
||||
export const WHERE_CLAUSE_CUSTOM_SUFFIX = '-custom';
|
||||
|
||||
export const useOptions = (
|
||||
key: string,
|
||||
keys: BaseAutocompleteData[],
|
||||
operator: string,
|
||||
searchValue: string,
|
||||
isMulti: boolean,
|
||||
isValidOperator: boolean,
|
||||
isExist: boolean,
|
||||
results: string[],
|
||||
result: string[],
|
||||
isFetching: boolean,
|
||||
whereClauseConfig?: WhereClauseConfig,
|
||||
isInfraMonitoring?: boolean,
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
): Option[] => {
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
const operators = useOperators(key, keys);
|
||||
|
||||
// get matching dynamic variables to suggest
|
||||
const dashboardDynamicVariables = useDashboardVariablesByType(
|
||||
'DYNAMIC',
|
||||
'values',
|
||||
);
|
||||
|
||||
const variableName = dashboardDynamicVariables?.find(
|
||||
(variable) => variable?.dynamicVariablesAttribute === key,
|
||||
)?.name;
|
||||
|
||||
const variableAsValue = variableName ? `$${variableName}` : '';
|
||||
|
||||
const getLabel = useCallback(
|
||||
(data: BaseAutocompleteData): Option['label'] => data?.key,
|
||||
[],
|
||||
);
|
||||
|
||||
const getOptionsFromKeys = useCallback(
|
||||
(items: BaseAutocompleteData[]): Option[] =>
|
||||
items?.map((item) => ({
|
||||
label: `${getLabel(item)}`,
|
||||
value: item.key,
|
||||
dataType: item.dataType,
|
||||
isIndexed: item?.isIndexed,
|
||||
type: item?.type || '',
|
||||
})),
|
||||
[getLabel],
|
||||
);
|
||||
|
||||
const getKeyOpValue = useCallback(
|
||||
(items: string[]): Option[] =>
|
||||
items?.map((item) => ({
|
||||
label: `${key} ${operator} ${item}`,
|
||||
value: `${key} ${operator} ${item}`,
|
||||
})),
|
||||
[key, operator],
|
||||
);
|
||||
|
||||
const getOptionsWithValidOperator = useCallback(
|
||||
(key: string, results: string[], searchValue: string) => {
|
||||
const hasAllResults = results.every((value) => result.includes(value));
|
||||
|
||||
let newResults = results;
|
||||
if (!isEmpty(variableAsValue)) {
|
||||
newResults = [variableAsValue, ...newResults];
|
||||
}
|
||||
|
||||
const values = getKeyOpValue(newResults);
|
||||
|
||||
return hasAllResults
|
||||
? [
|
||||
{
|
||||
label: searchValue,
|
||||
value: searchValue,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: searchValue,
|
||||
value: searchValue,
|
||||
},
|
||||
...values,
|
||||
];
|
||||
},
|
||||
[getKeyOpValue, result, variableAsValue],
|
||||
);
|
||||
|
||||
const getKeyOperatorOptions = useCallback(
|
||||
(key: string) => {
|
||||
const keyOperator = key.split(' ');
|
||||
const partialOperator = keyOperator?.[1];
|
||||
const partialKey = keyOperator?.[0];
|
||||
const filteredOperators = !isEmpty(partialOperator)
|
||||
? operators?.filter((operator) =>
|
||||
operator.startsWith(partialOperator?.toUpperCase()),
|
||||
)
|
||||
: operators;
|
||||
const operatorsOptions = filteredOperators?.map((op) => {
|
||||
const labelOp =
|
||||
isInfraMonitoring && INFRA_SHORT_TO_LONG_OPERATOR_MAP[op]
|
||||
? INFRA_SHORT_TO_LONG_OPERATOR_MAP[op]
|
||||
: op;
|
||||
return {
|
||||
value: `${partialKey} ${op} `,
|
||||
label: `${partialKey} ${labelOp} `,
|
||||
};
|
||||
});
|
||||
if (whereClauseConfig) {
|
||||
return [
|
||||
{
|
||||
label: `${searchValue} `,
|
||||
value: `${searchValue}${WHERE_CLAUSE_CUSTOM_SUFFIX}`,
|
||||
},
|
||||
...operatorsOptions,
|
||||
];
|
||||
}
|
||||
return operatorsOptions;
|
||||
},
|
||||
[isInfraMonitoring, operators, searchValue, whereClauseConfig],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let newOptions: Option[] = [];
|
||||
|
||||
if (!key) {
|
||||
newOptions = searchValue
|
||||
? [
|
||||
{
|
||||
label: `${searchValue} `,
|
||||
value: `${searchValue} `,
|
||||
},
|
||||
...getOptionsFromKeys(keys),
|
||||
]
|
||||
: getOptionsFromKeys(keys);
|
||||
} else if (key && !operator) {
|
||||
newOptions = getKeyOperatorOptions(key);
|
||||
} else if (key && operator) {
|
||||
if (isMulti) {
|
||||
const resultsWithVariable = isEmpty(variableAsValue)
|
||||
? results
|
||||
: [variableAsValue, ...results];
|
||||
newOptions = resultsWithVariable.map((item) => ({
|
||||
label: checkCommaInValue(String(item)),
|
||||
value: String(item),
|
||||
}));
|
||||
} else if (isExist) {
|
||||
newOptions = [];
|
||||
} else if (isValidOperator) {
|
||||
newOptions = getOptionsWithValidOperator(key, results, searchValue);
|
||||
}
|
||||
}
|
||||
if (newOptions.length > 0) {
|
||||
setOptions(newOptions);
|
||||
}
|
||||
if (isFetching) {
|
||||
setOptions([]);
|
||||
}
|
||||
}, [
|
||||
whereClauseConfig,
|
||||
getKeyOpValue,
|
||||
getOptionsFromKeys,
|
||||
isExist,
|
||||
isMulti,
|
||||
isValidOperator,
|
||||
key,
|
||||
keys,
|
||||
operator,
|
||||
operators,
|
||||
result,
|
||||
results,
|
||||
searchValue,
|
||||
getKeyOperatorOptions,
|
||||
getOptionsWithValidOperator,
|
||||
isFetching,
|
||||
variableAsValue,
|
||||
]);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
(
|
||||
options.filter(
|
||||
(option, index, self) =>
|
||||
index ===
|
||||
self.findIndex(
|
||||
(o) =>
|
||||
o.label === option.label &&
|
||||
o.value === option.value &&
|
||||
(o.type || '') === (option.type || '') &&
|
||||
(o.dataType || '') === (option.dataType || ''), // keep entries with same key but different type/dataType
|
||||
) && option.value !== '',
|
||||
) || []
|
||||
).map((option) => {
|
||||
const { tagValue } = getTagToken(searchValue);
|
||||
if (isMulti) {
|
||||
return {
|
||||
...option,
|
||||
selected: tagValue
|
||||
.filter((i) => i.trim().replace(/^\s+/, '') === option.value)
|
||||
.includes(option.value),
|
||||
};
|
||||
}
|
||||
return option;
|
||||
}),
|
||||
[isMulti, options, searchValue],
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
getRemovePrefixFromKey,
|
||||
getTagToken,
|
||||
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
type ICurrentKeyAndOperator = [string, string, string[]];
|
||||
|
||||
export const useSetCurrentKeyAndOperator = (
|
||||
value: string,
|
||||
keys: BaseAutocompleteData[],
|
||||
): ICurrentKeyAndOperator => {
|
||||
const [key, operator, result] = useMemo(() => {
|
||||
let key = '';
|
||||
let operator = '';
|
||||
let result: string[] = [];
|
||||
const { tagKey, tagOperator, tagValue } = getTagToken(value);
|
||||
const isSuggestKey = keys?.some(
|
||||
(el) => el?.key === getRemovePrefixFromKey(tagKey),
|
||||
);
|
||||
if (isSuggestKey || keys.length === 0) {
|
||||
key = tagKey || '';
|
||||
operator = tagOperator || '';
|
||||
result = tagValue || [];
|
||||
}
|
||||
|
||||
return [key, operator, result];
|
||||
}, [value, keys]);
|
||||
|
||||
return [key, operator, result];
|
||||
};
|
||||
@@ -1,18 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { INFRA_SHORT_TO_LONG_OPERATOR_MAP } from 'constants/queryBuilder';
|
||||
import {
|
||||
getOperatorFromValue,
|
||||
getTagToken,
|
||||
isExistsNotExistsOperator,
|
||||
isInNInOperator,
|
||||
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
} from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { unparse } from 'papaparse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { WhereClauseConfig } from './useAutoComplete';
|
||||
import type { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
/**
|
||||
* Helper for formatting a TagFilter object into filter item strings
|
||||
@@ -42,95 +34,3 @@ export function queryFilterTags(
|
||||
return `${ele.key?.key} ${displayOp} ${ele.value}`;
|
||||
});
|
||||
}
|
||||
|
||||
type IUseTag = {
|
||||
handleAddTag: (value: string) => void;
|
||||
handleClearTag: (value: string) => void;
|
||||
tags: string[];
|
||||
updateTag: (value: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A custom React hook for handling tags.
|
||||
* @param {string} key - A string value to identify tags.
|
||||
* @param {boolean} isValidTag - A boolean value to indicate whether the tag is valid.
|
||||
* @param {function} handleSearch - A callback function to handle search.
|
||||
* @returns {IUseTag} The return object containing handlers and tags.
|
||||
*/
|
||||
|
||||
export const useTag = (
|
||||
isValidTag: boolean,
|
||||
handleSearch: (value: string) => void,
|
||||
query: IBuilderQuery,
|
||||
setSearchKey: (value: string) => void,
|
||||
whereClauseConfig?: WhereClauseConfig,
|
||||
isInfraMonitoring?: boolean,
|
||||
): IUseTag => {
|
||||
const initTagsData = useMemo(
|
||||
() =>
|
||||
queryFilterTags(
|
||||
query?.filters || { items: [], op: 'AND' },
|
||||
isInfraMonitoring,
|
||||
),
|
||||
[query?.filters, isInfraMonitoring],
|
||||
);
|
||||
|
||||
const [tags, setTags] = useState<string[]>(initTagsData);
|
||||
|
||||
const updateTag = (value: string): void => {
|
||||
const newTags = tags?.filter((item: string) => item !== value);
|
||||
setTags(newTags);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a new tag to the tag list.
|
||||
* @param {string} value - The tag value to be added.
|
||||
*/
|
||||
|
||||
const handleAddTag = useCallback(
|
||||
(value: string): void => {
|
||||
const { tagKey } = getTagToken(value);
|
||||
const parts = tagKey.split('-');
|
||||
// this is done to ensure that `hello-world` also gets converted to `body CONTAINS hello-world`
|
||||
let id = parts[parts.length - 1];
|
||||
let key = parts.slice(0, -1).join('-');
|
||||
if (parts.length === 1) {
|
||||
id = '';
|
||||
[key] = parts;
|
||||
}
|
||||
|
||||
if (id === 'custom') {
|
||||
const customValue = whereClauseConfig
|
||||
? `${whereClauseConfig.customKey} ${whereClauseConfig.customOp} ${key}`
|
||||
: '';
|
||||
setTags((prevTags) =>
|
||||
prevTags.includes(customValue) ? prevTags : [...prevTags, customValue],
|
||||
);
|
||||
handleSearch('');
|
||||
setSearchKey('');
|
||||
return;
|
||||
}
|
||||
|
||||
if ((value && key && isValidTag) || isExistsNotExistsOperator(value)) {
|
||||
setTags((prevTags) => [...prevTags, value]);
|
||||
handleSearch('');
|
||||
setSearchKey('');
|
||||
}
|
||||
},
|
||||
[whereClauseConfig, isValidTag, handleSearch, setSearchKey],
|
||||
);
|
||||
|
||||
/**
|
||||
* Removes a tag from the tag list.
|
||||
* @param {string} value - The tag value to be removed.
|
||||
*/
|
||||
const handleClearTag = useCallback((value: string): void => {
|
||||
setTags((prevTags) => prevTags.filter((v) => v !== value));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTags(initTagsData);
|
||||
}, [initTagsData]);
|
||||
|
||||
return { handleAddTag, handleClearTag, tags, updateTag };
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBui
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||
|
||||
@@ -16,7 +16,7 @@ import LogsError from 'container/LogsError/LogsError';
|
||||
import { EmptyLogsListConfig } from 'container/LogsExplorerList/utils';
|
||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { Compass } from '@signozhq/icons';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBui
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { PANEL_TYPES, QueryBuilderKeys } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||
import { ArrowDownToDot, ArrowUpFromDot } from '@signozhq/icons';
|
||||
|
||||
@@ -1,99 +1,269 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/auth_domains", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.List), handler.OpenAPIDef{
|
||||
ID: "ListAuthDomains",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "List all auth domains",
|
||||
Description: "This endpoint lists all auth domains",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: make([]*authtypes.GettableAuthDomain, 0),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/auth_domains", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.authDomainHandler.List, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListAuthDomains",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "List all auth domains",
|
||||
Description: "This endpoint lists all auth domains",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: make([]*authtypes.GettableAuthDomain, 0),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceAuthDomain.Scope(coretypes.VerbList)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceAuthDomain,
|
||||
Verb: coretypes.VerbList,
|
||||
Category: coretypes.ActionCategoryAccessControl,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}),
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/auth_domains", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Create), handler.OpenAPIDef{
|
||||
ID: "CreateAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Create auth domain",
|
||||
Description: "This endpoint creates an auth domain",
|
||||
Request: new(authtypes.PostableAuthDomain),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/auth_domains", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.authDomainHandler.Create, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Create auth domain",
|
||||
Description: "This endpoint creates an auth domain",
|
||||
Request: new(authtypes.PostableAuthDomain),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{
|
||||
coretypes.ResourceMetaResourceAuthDomain.Scope(coretypes.VerbCreate),
|
||||
coretypes.ResourceMetaResourceAuthDomain.Scope(coretypes.VerbAttach),
|
||||
coretypes.ResourceRole.Scope(coretypes.VerbAttach),
|
||||
}),
|
||||
},
|
||||
handler.WithResourceDefs(
|
||||
handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceAuthDomain,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: coretypes.ActionCategoryAccessControl,
|
||||
ID: coretypes.ResponseJSONPath("data.id"),
|
||||
Selector: coretypes.WildcardSelector,
|
||||
},
|
||||
handler.AttachDetachSiblingResourceDef{
|
||||
Verb: coretypes.VerbAttach,
|
||||
Category: coretypes.ActionCategoryAccessControl,
|
||||
SourceResource: coretypes.ResourceMetaResourceAuthDomain,
|
||||
SourceIDs: coretypes.OneID(coretypes.ResponseJSONPath("data.id")),
|
||||
SourceSelector: coretypes.WildcardSelector,
|
||||
TargetResource: coretypes.ResourceRole,
|
||||
TargetIDs: authDomainRoleNamesExtractor(),
|
||||
TargetSelector: coretypes.IDSelector,
|
||||
},
|
||||
),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/auth_domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Get), handler.OpenAPIDef{
|
||||
ID: "GetAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Get auth domain by ID",
|
||||
Description: "This endpoint returns an auth domain by ID",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(authtypes.GettableAuthDomain),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/auth_domains/{id}", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.authDomainHandler.Get, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Get auth domain by ID",
|
||||
Description: "This endpoint returns an auth domain by ID",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(authtypes.GettableAuthDomain),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceAuthDomain.Scope(coretypes.VerbRead)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceAuthDomain,
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryAccessControl,
|
||||
ID: coretypes.PathParam("id"),
|
||||
Selector: coretypes.IDSelector,
|
||||
}),
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/auth_domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Update), handler.OpenAPIDef{
|
||||
ID: "UpdateAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Update auth domain",
|
||||
Description: "This endpoint updates an auth domain",
|
||||
Request: new(authtypes.UpdatableAuthDomain),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/auth_domains/{id}", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.authDomainHandler.Update, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Update auth domain",
|
||||
Description: "This endpoint updates an auth domain",
|
||||
Request: new(authtypes.UpdatableAuthDomain),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{
|
||||
coretypes.ResourceMetaResourceAuthDomain.Scope(coretypes.VerbUpdate),
|
||||
coretypes.ResourceMetaResourceAuthDomain.Scope(coretypes.VerbAttach),
|
||||
coretypes.ResourceMetaResourceAuthDomain.Scope(coretypes.VerbDetach),
|
||||
coretypes.ResourceRole.Scope(coretypes.VerbAttach),
|
||||
coretypes.ResourceRole.Scope(coretypes.VerbDetach),
|
||||
}),
|
||||
},
|
||||
handler.WithResourceDefs(
|
||||
handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceAuthDomain,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: coretypes.ActionCategoryAccessControl,
|
||||
ID: coretypes.PathParam("id"),
|
||||
Selector: coretypes.IDSelector,
|
||||
},
|
||||
handler.AttachDetachSiblingResourceDef{
|
||||
Verb: coretypes.VerbAttach,
|
||||
Category: coretypes.ActionCategoryAccessControl,
|
||||
SourceResource: coretypes.ResourceMetaResourceAuthDomain,
|
||||
SourceIDs: coretypes.OneID(coretypes.PathParam("id")),
|
||||
SourceSelector: coretypes.IDSelector,
|
||||
TargetResource: coretypes.ResourceRole,
|
||||
TargetIDs: authDomainRoleNamesExtractor(),
|
||||
TargetSelector: coretypes.IDSelector,
|
||||
},
|
||||
handler.AttachDetachSiblingResourceDef{
|
||||
Verb: coretypes.VerbDetach,
|
||||
Category: coretypes.ActionCategoryAccessControl,
|
||||
SourceResource: coretypes.ResourceMetaResourceAuthDomain,
|
||||
SourceIDs: coretypes.OneID(coretypes.PathParam("id")),
|
||||
SourceSelector: coretypes.IDSelector,
|
||||
TargetResource: coretypes.ResourceRole,
|
||||
TargetIDs: provider.authDomainStoredRoleNamesExtractor(),
|
||||
TargetSelector: coretypes.IDSelector,
|
||||
},
|
||||
),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/auth_domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Delete), handler.OpenAPIDef{
|
||||
ID: "DeleteAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Delete auth domain",
|
||||
Description: "This endpoint deletes an auth domain",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/auth_domains/{id}", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.authDomainHandler.Delete, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Delete auth domain",
|
||||
Description: "This endpoint deletes an auth domain",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceAuthDomain.Scope(coretypes.VerbDelete)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceAuthDomain,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: coretypes.ActionCategoryAccessControl,
|
||||
ID: coretypes.PathParam("id"),
|
||||
Selector: coretypes.IDSelector,
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// The extracted names are the roles the request body's mapping grants at SSO
|
||||
// login — see authDomainEffectiveRoleNames.
|
||||
func authDomainRoleNamesExtractor() coretypes.ResourceIDsExtractor {
|
||||
return coretypes.ResourceIDsExtractor{Phase: coretypes.PhaseRequest, Fn: func(ec coretypes.ExtractorContext) ([]string, error) {
|
||||
roleMappingJSON := gjson.GetBytes(ec.RequestBody, "roleMapping")
|
||||
if !roleMappingJSON.Exists() || roleMappingJSON.Type == gjson.Null {
|
||||
return authDomainEffectiveRoleNames(nil), nil
|
||||
}
|
||||
|
||||
roleMapping := new(authtypes.RoleMapping)
|
||||
if err := json.Unmarshal([]byte(roleMappingJSON.Raw), roleMapping); err != nil {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid role mapping: %v", err)
|
||||
}
|
||||
|
||||
return authDomainEffectiveRoleNames(roleMapping), nil
|
||||
}}
|
||||
}
|
||||
|
||||
// The extracted names are the roles the stored domain's mapping grants at SSO
|
||||
// login — an update replaces that mapping, so the caller must be able to detach
|
||||
// them.
|
||||
func (provider *provider) authDomainStoredRoleNamesExtractor() coretypes.ResourceIDsExtractor {
|
||||
return coretypes.ResourceIDsExtractor{Phase: coretypes.PhaseRequest, Fn: func(ec coretypes.ExtractorContext) ([]string, error) {
|
||||
if ec.Request == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ec.Request.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := valuer.NewUUID(mux.Vars(ec.Request)["id"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authDomain, err := provider.authDomainModule.GetByOrgIDAndID(ec.Request.Context(), orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return authDomainEffectiveRoleNames(authDomain.RoleMapping()), nil
|
||||
}}
|
||||
}
|
||||
|
||||
// The effective names are the roles a domain grants at SSO login: the mapped
|
||||
// roles plus the default (signoz-viewer when unset), or every role when the IDP
|
||||
// role attribute is trusted. Never empty — a check with no selectors is forbidden.
|
||||
func authDomainEffectiveRoleNames(roleMapping *authtypes.RoleMapping) []string {
|
||||
if roleMapping == nil {
|
||||
return []string{authtypes.SigNozViewerRoleName}
|
||||
}
|
||||
|
||||
if roleMapping.UseRoleAttribute {
|
||||
return []string{coretypes.WildCardSelectorString}
|
||||
}
|
||||
|
||||
roleNames := roleMapping.RoleNames()
|
||||
if !slices.Contains(roleNames, roleMapping.DefaultRoleName()) {
|
||||
roleNames = append(roleNames, roleMapping.DefaultRoleName())
|
||||
}
|
||||
|
||||
return roleNames
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ type provider struct {
|
||||
userHandler user.Handler
|
||||
sessionHandler session.Handler
|
||||
authDomainHandler authdomain.Handler
|
||||
authDomainModule authdomain.Module
|
||||
preferenceHandler preference.Handler
|
||||
globalHandler global.Handler
|
||||
promoteHandler promote.Handler
|
||||
@@ -88,6 +89,7 @@ func NewFactory(
|
||||
userHandler user.Handler,
|
||||
sessionHandler session.Handler,
|
||||
authDomainHandler authdomain.Handler,
|
||||
authDomainModule authdomain.Module,
|
||||
preferenceHandler preference.Handler,
|
||||
globalHandler global.Handler,
|
||||
promoteHandler promote.Handler,
|
||||
@@ -128,6 +130,7 @@ func NewFactory(
|
||||
userHandler,
|
||||
sessionHandler,
|
||||
authDomainHandler,
|
||||
authDomainModule,
|
||||
preferenceHandler,
|
||||
globalHandler,
|
||||
promoteHandler,
|
||||
@@ -170,6 +173,7 @@ func newProvider(
|
||||
userHandler user.Handler,
|
||||
sessionHandler session.Handler,
|
||||
authDomainHandler authdomain.Handler,
|
||||
authDomainModule authdomain.Module,
|
||||
preferenceHandler preference.Handler,
|
||||
globalHandler global.Handler,
|
||||
promoteHandler promote.Handler,
|
||||
@@ -211,6 +215,7 @@ func newProvider(
|
||||
authzService: authzService,
|
||||
sessionHandler: sessionHandler,
|
||||
authDomainHandler: authDomainHandler,
|
||||
authDomainModule: authDomainModule,
|
||||
preferenceHandler: preferenceHandler,
|
||||
globalHandler: globalHandler,
|
||||
promoteHandler: promoteHandler,
|
||||
|
||||
@@ -138,66 +138,6 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.SetRole, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateServiceAccountRoleDeprecated",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Create service account role",
|
||||
Description: "This endpoint assigns a role to a service account",
|
||||
Request: new(serviceaccounttypes.DeprecatedPostableServiceAccountRole),
|
||||
RequestContentType: "",
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: true,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach), coretypes.ResourceRole.Scope(coretypes.VerbAttach)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.AttachDetachSiblingResourceDef{
|
||||
Verb: coretypes.VerbAttach,
|
||||
Category: coretypes.ActionCategoryAccessControl,
|
||||
SourceResource: coretypes.ResourceServiceAccount,
|
||||
SourceIDs: coretypes.OneID(coretypes.PathParam("id")),
|
||||
SourceSelector: coretypes.IDSelector,
|
||||
TargetResource: coretypes.ResourceRole,
|
||||
TargetIDs: coretypes.OneID(coretypes.BodyJSONPath("id")),
|
||||
TargetSelector: provider.roleSelector,
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/roles/{rid}", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.DeleteRole, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteServiceAccountRoleDeprecated",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Delete service account role",
|
||||
Description: "This endpoint revokes a role from service account",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: true,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbDetach), coretypes.ResourceRole.Scope(coretypes.VerbDetach)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.AttachDetachSiblingResourceDef{
|
||||
Verb: coretypes.VerbDetach,
|
||||
Category: coretypes.ActionCategoryAccessControl,
|
||||
SourceResource: coretypes.ResourceServiceAccount,
|
||||
SourceIDs: coretypes.OneID(coretypes.PathParam("id")),
|
||||
SourceSelector: coretypes.IDSelector,
|
||||
TargetResource: coretypes.ResourceRole,
|
||||
TargetIDs: coretypes.OneID(coretypes.PathParam("rid")),
|
||||
TargetSelector: provider.roleSelector,
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/me", handler.New(provider.authzMiddleware.OpenAccess(provider.serviceAccountHandler.UpdateMe), handler.OpenAPIDef{
|
||||
ID: "UpdateMyServiceAccount",
|
||||
Tags: []string{"serviceaccount"},
|
||||
|
||||
@@ -214,64 +214,6 @@ func (handler *handler) UpdateMe(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) SetRole(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := valuer.NewUUID(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := new(serviceaccounttypes.DeprecatedPostableServiceAccountRole)
|
||||
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.module.SetRole(ctx, valuer.MustNewUUID(claims.OrgID), id, req.ID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) DeleteRole(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := valuer.NewUUID(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
roleID, err := valuer.NewUUID(mux.Vars(r)["rid"])
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.module.DeleteRole(ctx, valuer.MustNewUUID(claims.OrgID), id, roleID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) CreateServiceAccountRole(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
|
||||
@@ -94,10 +94,6 @@ type Handler interface {
|
||||
|
||||
UpdateMe(http.ResponseWriter, *http.Request)
|
||||
|
||||
SetRole(http.ResponseWriter, *http.Request)
|
||||
|
||||
DeleteRole(http.ResponseWriter, *http.Request)
|
||||
|
||||
CreateServiceAccountRole(http.ResponseWriter, *http.Request)
|
||||
|
||||
GetServiceAccountRole(http.ResponseWriter, *http.Request)
|
||||
|
||||
@@ -64,6 +64,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ user.Handler }{},
|
||||
struct{ session.Handler }{},
|
||||
struct{ authdomain.Handler }{},
|
||||
struct{ authdomain.Module }{},
|
||||
struct{ preference.Handler }{},
|
||||
struct{ global.Handler }{},
|
||||
struct{ promote.Handler }{},
|
||||
|
||||
@@ -243,6 +243,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewFixSavedViewSelectFieldsFactory(sqlstore),
|
||||
sqlmigration.NewDeleteOrphanUserRolesFactory(),
|
||||
sqlmigration.NewMigrateLambdaDashboardsFactory(),
|
||||
sqlmigration.NewAddAuthDomainTuplesFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -317,6 +318,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
impluser.NewHandler(modules.UserSetter, modules.UserGetter),
|
||||
implsession.NewHandler(modules.Session, globalConfig),
|
||||
implauthdomain.NewHandler(modules.AuthDomain),
|
||||
modules.AuthDomain,
|
||||
implpreference.NewHandler(modules.Preference),
|
||||
handlers.Global,
|
||||
implpromote.NewHandler(modules.Promote),
|
||||
|
||||
167
pkg/sqlmigration/117_add_auth_domain_tuples.go
Normal file
167
pkg/sqlmigration/117_add_auth_domain_tuples.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addAuthDomainTuples struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewAddAuthDomainTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_auth_domain_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addAuthDomainTuples{sqlstore: sqlstore}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addAuthDomainTuples) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addAuthDomainTuples) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var storeID string
|
||||
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var orgIDs []string
|
||||
err = tx.NewSelect().
|
||||
Table("organizations").
|
||||
Column("id").
|
||||
Scan(ctx, &orgIDs)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
||||
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
|
||||
|
||||
// auth-domain moved from the legacy AdminAccess role gate to CheckResources,
|
||||
// which on enterprise requires real tuples -- existing orgs never had these
|
||||
// written, only new orgs get them from the registry at bootstrap.
|
||||
tuples := []migrationTuple{
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "auth-domain", "create"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "auth-domain", "read"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "auth-domain", "update"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "auth-domain", "delete"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "auth-domain", "list"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "auth-domain", "attach"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "auth-domain", "detach"},
|
||||
}
|
||||
|
||||
for _, orgID := range orgIDs {
|
||||
for _, tuple := range tuples {
|
||||
entropy := ulid.DefaultEntropy()
|
||||
now := time.Now().UTC()
|
||||
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
|
||||
|
||||
objectID := "organization/" + orgID + "/" + tuple.objectName + "/*"
|
||||
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
|
||||
|
||||
if isPG {
|
||||
user := "role:" + roleSubject + "#assignee"
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, user, 0, tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The auth-domain attach/detach transactions are new to the registry, so the
|
||||
// managed-role transaction groups stored per org (served by the roles API)
|
||||
// must be re-synced from it, like 105_update_role_transaction_groups did.
|
||||
managedRoleGroups := make(map[string]string, len(coretypes.ManagedRoleToTransactions))
|
||||
for roleName, transactions := range coretypes.ManagedRoleToTransactions {
|
||||
data, err := json.Marshal(authtypes.NewTransactionGroupsFromTransactions(transactions))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
managedRoleGroups[roleName] = string(data)
|
||||
}
|
||||
|
||||
for _, orgID := range orgIDs {
|
||||
for roleName, data := range managedRoleGroups {
|
||||
if _, err := tx.NewUpdate().
|
||||
Model(new(roles)).
|
||||
Set("transaction_groups = ?", data).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("type = ?", authtypes.RoleTypeManaged.StringValue()).
|
||||
Where("name = ?", roleName).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addAuthDomainTuples) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -33,6 +33,8 @@ var ManagedRoleToTransactions = map[string][]Transaction{
|
||||
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindAuthDomain}, WildCardSelectorString)},
|
||||
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindAuthDomain}, WildCardSelectorString)},
|
||||
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindAuthDomain}, WildCardSelectorString)},
|
||||
{Verb: VerbAttach, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindAuthDomain}, WildCardSelectorString)},
|
||||
{Verb: VerbDetach, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindAuthDomain}, WildCardSelectorString)},
|
||||
// cloud-integration — admin only
|
||||
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegration}, WildCardSelectorString)},
|
||||
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegration}, WildCardSelectorString)},
|
||||
|
||||
@@ -1274,6 +1274,264 @@ func TestTimeSeriesPanelDefaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAreaChartPanelDefaults(t *testing.T) {
|
||||
data := []byte(`{
|
||||
"variables": [],
|
||||
"panels": {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/AreaChartPanel",
|
||||
"spec": {}
|
||||
},
|
||||
"queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
d, err := unmarshalDashboard(data)
|
||||
require.NoError(t, err, "unmarshal and validate failed")
|
||||
|
||||
require.IsType(t, &AreaChartPanelSpec{}, d.Panels["p1"].Spec.Plugin.Spec)
|
||||
spec := d.Panels["p1"].Spec.Plugin.Spec.(*AreaChartPanelSpec)
|
||||
|
||||
assert.Equal(t, "solid", spec.ChartAppearance.FillMode.ValueOrDefault(), "area fillMode defaults to solid, where the TimeSeries FillMode defaults to none")
|
||||
assert.Nil(t, spec.ChartAppearance.FillOpacity, "an omitted fillOpacity stays nil so the renderer applies the kind default")
|
||||
assert.Equal(t, "none", spec.Visualization.Stack.ValueOrDefault(), "expected Stack default none")
|
||||
assert.Equal(t, "2", spec.Formatting.DecimalPrecision.ValueOrDefault(), "expected DecimalPrecision default 2")
|
||||
assert.Equal(t, "spline", spec.ChartAppearance.LineInterpolation.ValueOrDefault(), "expected LineInterpolation default spline")
|
||||
assert.Equal(t, "solid", spec.ChartAppearance.LineStyle.ValueOrDefault(), "expected LineStyle default solid")
|
||||
assert.Equal(t, "global_time", spec.Visualization.TimePreference.ValueOrDefault(), "expected TimePreference default global_time")
|
||||
assert.Equal(t, "bottom", spec.Legend.Position.ValueOrDefault(), "expected LegendPosition default bottom")
|
||||
assert.Equal(t, "list", spec.Legend.Mode.ValueOrDefault(), "expected LegendMode default list")
|
||||
|
||||
output, err := json.Marshal(d)
|
||||
require.NoError(t, err, "marshal dashboard failed")
|
||||
outputStr := string(output)
|
||||
for field, want := range map[string]string{
|
||||
"fillMode": `"solid"`,
|
||||
"stack": `"none"`,
|
||||
"fillOpacity": `null`,
|
||||
} {
|
||||
assert.Contains(t, outputStr, `"`+field+`":`+want, "expected stored/response JSON to contain %s:%s", field, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAreaChartPanelRoundTrip(t *testing.T) {
|
||||
data := []byte(`{
|
||||
"variables": [],
|
||||
"panels": {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/AreaChartPanel",
|
||||
"spec": {
|
||||
"visualization": {"timePreference": "global_time", "fillSpans": false, "stack": "percent"},
|
||||
"chartAppearance": {"fillMode": "gradient", "fillOpacity": 0.4}
|
||||
}
|
||||
},
|
||||
"queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
d, err := unmarshalDashboard(data)
|
||||
require.NoError(t, err, "unmarshal and validate failed")
|
||||
|
||||
require.IsType(t, &AreaChartPanelSpec{}, d.Panels["p1"].Spec.Plugin.Spec)
|
||||
spec := d.Panels["p1"].Spec.Plugin.Spec.(*AreaChartPanelSpec)
|
||||
|
||||
assert.Equal(t, "percent", spec.Visualization.Stack.ValueOrDefault(), "expected stack percent")
|
||||
assert.Equal(t, "gradient", spec.ChartAppearance.FillMode.ValueOrDefault(), "expected fillMode gradient")
|
||||
|
||||
output, err := json.Marshal(d)
|
||||
require.NoError(t, err, "marshal dashboard failed")
|
||||
assert.Contains(t, string(output), `"stack":"percent"`, "expected stack in stored/response JSON")
|
||||
assert.Contains(t, string(output), `"fillMode":"gradient"`, "expected fillMode in stored/response JSON")
|
||||
}
|
||||
|
||||
func TestAreaChartPanelFillOpacity(t *testing.T) {
|
||||
tests := []struct {
|
||||
scenario string
|
||||
chartAppearance string
|
||||
expectedFillOpacitySet bool
|
||||
expectedFillOpacityValue FillOpacity
|
||||
expectedMarshalledJSON string
|
||||
}{
|
||||
{
|
||||
scenario: "zero is a set value, not an absent one",
|
||||
chartAppearance: `{"fillOpacity": 0}`,
|
||||
expectedFillOpacitySet: true,
|
||||
expectedFillOpacityValue: 0,
|
||||
expectedMarshalledJSON: `"fillOpacity":0`,
|
||||
},
|
||||
{
|
||||
scenario: "fully opaque upper bound",
|
||||
chartAppearance: `{"fillOpacity": 1}`,
|
||||
expectedFillOpacitySet: true,
|
||||
expectedFillOpacityValue: 1,
|
||||
expectedMarshalledJSON: `"fillOpacity":1`,
|
||||
},
|
||||
{
|
||||
scenario: "typical fractional value",
|
||||
chartAppearance: `{"fillOpacity": 0.4}`,
|
||||
expectedFillOpacitySet: true,
|
||||
expectedFillOpacityValue: 0.4,
|
||||
expectedMarshalledJSON: `"fillOpacity":0.4`,
|
||||
},
|
||||
{
|
||||
scenario: "precision beyond one decimal place survives",
|
||||
chartAppearance: `{"fillOpacity": 0.125}`,
|
||||
expectedFillOpacitySet: true,
|
||||
expectedFillOpacityValue: 0.125,
|
||||
expectedMarshalledJSON: `"fillOpacity":0.125`,
|
||||
},
|
||||
{
|
||||
scenario: "omitted field stays nil so the renderer applies the kind default",
|
||||
chartAppearance: `{}`,
|
||||
expectedFillOpacitySet: false,
|
||||
expectedMarshalledJSON: `"fillOpacity":null`,
|
||||
},
|
||||
{
|
||||
scenario: "explicit null stays nil rather than decoding as zero",
|
||||
chartAppearance: `{"fillOpacity": null}`,
|
||||
expectedFillOpacitySet: false,
|
||||
expectedMarshalledJSON: `"fillOpacity":null`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.scenario, func(t *testing.T) {
|
||||
data := []byte(`{
|
||||
"variables": [],
|
||||
"panels": {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/AreaChartPanel", "spec": {"chartAppearance": ` + test.chartAppearance + `}},
|
||||
"queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
d, err := unmarshalDashboard(data)
|
||||
require.NoError(t, err, "unmarshal and validate failed")
|
||||
|
||||
require.IsType(t, &AreaChartPanelSpec{}, d.Panels["p1"].Spec.Plugin.Spec)
|
||||
spec := d.Panels["p1"].Spec.Plugin.Spec.(*AreaChartPanelSpec)
|
||||
|
||||
if !test.expectedFillOpacitySet {
|
||||
assert.Nil(t, spec.ChartAppearance.FillOpacity, "expected fillOpacity to stay unset")
|
||||
} else {
|
||||
require.NotNil(t, spec.ChartAppearance.FillOpacity, "expected fillOpacity to decode as a set value")
|
||||
assert.Equal(t, test.expectedFillOpacityValue, *spec.ChartAppearance.FillOpacity, "unexpected decoded fillOpacity")
|
||||
}
|
||||
|
||||
output, err := json.Marshal(d)
|
||||
require.NoError(t, err, "marshal dashboard failed")
|
||||
assert.Contains(t, string(output), test.expectedMarshalledJSON, "unexpected fillOpacity in stored/response JSON")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidateAreaChartPanelSpecValues(t *testing.T) {
|
||||
tests := []struct {
|
||||
scenario string
|
||||
panelKind string
|
||||
panelSpec string
|
||||
expectedErrorSubstring string
|
||||
}{
|
||||
{
|
||||
scenario: "unknown stack mode",
|
||||
panelKind: "signoz/AreaChartPanel",
|
||||
panelSpec: `{"visualization": {"stack": "stacked"}}`,
|
||||
expectedErrorSubstring: "stack mode",
|
||||
},
|
||||
{
|
||||
scenario: "unknown area fill mode",
|
||||
panelKind: "signoz/AreaChartPanel",
|
||||
panelSpec: `{"chartAppearance": {"fillMode": "striped"}}`,
|
||||
expectedErrorSubstring: "fill mode",
|
||||
},
|
||||
{
|
||||
scenario: "fill opacity on a 0-100 scale",
|
||||
panelKind: "signoz/AreaChartPanel",
|
||||
panelSpec: `{"chartAppearance": {"fillOpacity": 40}}`,
|
||||
expectedErrorSubstring: "invalid fillOpacity 40: must be between 0 and 1",
|
||||
},
|
||||
{
|
||||
scenario: "negative fill opacity",
|
||||
panelKind: "signoz/AreaChartPanel",
|
||||
panelSpec: `{"chartAppearance": {"fillOpacity": -0.5}}`,
|
||||
expectedErrorSubstring: "invalid fillOpacity -0.5: must be between 0 and 1",
|
||||
},
|
||||
{
|
||||
scenario: "non-numeric fill opacity",
|
||||
panelKind: "signoz/AreaChartPanel",
|
||||
panelSpec: `{"chartAppearance": {"fillOpacity": "0.4"}}`,
|
||||
expectedErrorSubstring: "cannot unmarshal string",
|
||||
},
|
||||
{
|
||||
scenario: "stack on a time series panel",
|
||||
panelKind: "signoz/TimeSeriesPanel",
|
||||
panelSpec: `{"visualization": {"stack": "normal"}}`,
|
||||
expectedErrorSubstring: `unknown field`,
|
||||
},
|
||||
{
|
||||
scenario: "fill opacity on a time series panel",
|
||||
panelKind: "signoz/TimeSeriesPanel",
|
||||
panelSpec: `{"chartAppearance": {"fillOpacity": 0.4}}`,
|
||||
expectedErrorSubstring: `unknown field`,
|
||||
},
|
||||
{
|
||||
scenario: "stacked bar chart on an area panel",
|
||||
panelKind: "signoz/AreaChartPanel",
|
||||
panelSpec: `{"visualization": {"stackedBarChart": true}}`,
|
||||
expectedErrorSubstring: `unknown field`,
|
||||
},
|
||||
{
|
||||
scenario: "stack on a bar chart panel",
|
||||
panelKind: "signoz/BarChartPanel",
|
||||
panelSpec: `{"visualization": {"stack": "percent"}}`,
|
||||
expectedErrorSubstring: `unknown field`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.scenario, func(t *testing.T) {
|
||||
data := []byte(`{
|
||||
"variables": [],
|
||||
"panels": {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "` + test.panelKind + `", "spec": ` + test.panelSpec + `},
|
||||
"queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
_, err := unmarshalDashboard(data)
|
||||
require.Error(t, err, "expected the spec to be rejected")
|
||||
assert.Contains(t, err.Error(), test.expectedErrorSubstring, "unexpected error message: %s", err.Error())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumberPanelDefaults(t *testing.T) {
|
||||
data := []byte(`{
|
||||
"variables": [],
|
||||
|
||||
@@ -30,6 +30,7 @@ func (PanelPlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
return markDiscriminator(s, "kind", map[string]string{
|
||||
string(PanelKindTimeSeries): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec"),
|
||||
string(PanelKindBarChart): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec"),
|
||||
string(PanelKindAreaChart): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec"),
|
||||
string(PanelKindNumber): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpec"),
|
||||
string(PanelKindPieChart): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpec"),
|
||||
string(PanelKindTable): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec"),
|
||||
@@ -60,6 +61,7 @@ func (PanelPlugin) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
PanelPluginVariant[TimeSeriesPanelSpec]{Kind: string(PanelKindTimeSeries)},
|
||||
PanelPluginVariant[BarChartPanelSpec]{Kind: string(PanelKindBarChart)},
|
||||
PanelPluginVariant[AreaChartPanelSpec]{Kind: string(PanelKindAreaChart)},
|
||||
PanelPluginVariant[NumberPanelSpec]{Kind: string(PanelKindNumber)},
|
||||
PanelPluginVariant[PieChartPanelSpec]{Kind: string(PanelKindPieChart)},
|
||||
PanelPluginVariant[TablePanelSpec]{Kind: string(PanelKindTable)},
|
||||
@@ -223,6 +225,7 @@ var (
|
||||
panelPluginSpecs = map[PanelPluginKind]func() any{
|
||||
PanelKindTimeSeries: func() any { return new(TimeSeriesPanelSpec) },
|
||||
PanelKindBarChart: func() any { return new(BarChartPanelSpec) },
|
||||
PanelKindAreaChart: func() any { return new(AreaChartPanelSpec) },
|
||||
PanelKindNumber: func() any { return new(NumberPanelSpec) },
|
||||
PanelKindPieChart: func() any { return new(PieChartPanelSpec) },
|
||||
PanelKindTable: func() any { return new(TablePanelSpec) },
|
||||
@@ -245,6 +248,7 @@ var (
|
||||
allowedQueryKinds = map[PanelPluginKind][]QueryPluginKind{
|
||||
PanelKindTimeSeries: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindBarChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindAreaChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindNumber: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindHistogram: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
|
||||
PanelKindPieChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
|
||||
|
||||
@@ -183,7 +183,8 @@ func (d *DashboardV2) GetPanelQuery(startTime, endTime uint64, panelKey string)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// fillGaps lives on the panel visualization; only timeseries and bar chart carry it.
|
||||
// fillGaps lives on the panel visualization; only timeseries, bar chart and
|
||||
// area chart carry it.
|
||||
fillGaps := false
|
||||
switch panelSpec := panel.Spec.Plugin.Spec.(type) {
|
||||
case *TimeSeriesPanelSpec:
|
||||
@@ -194,6 +195,10 @@ func (d *DashboardV2) GetPanelQuery(startTime, endTime uint64, panelKey string)
|
||||
if panelSpec != nil {
|
||||
fillGaps = panelSpec.Visualization.FillSpans
|
||||
}
|
||||
case *AreaChartPanelSpec:
|
||||
if panelSpec != nil {
|
||||
fillGaps = panelSpec.Visualization.FillSpans
|
||||
}
|
||||
}
|
||||
|
||||
return &qb.QueryRangeRequest{
|
||||
|
||||
@@ -168,6 +168,7 @@ type PanelPluginKind string
|
||||
const (
|
||||
PanelKindTimeSeries PanelPluginKind = "signoz/TimeSeriesPanel"
|
||||
PanelKindBarChart PanelPluginKind = "signoz/BarChartPanel"
|
||||
PanelKindAreaChart PanelPluginKind = "signoz/AreaChartPanel"
|
||||
PanelKindNumber PanelPluginKind = "signoz/NumberPanel"
|
||||
PanelKindPieChart PanelPluginKind = "signoz/PieChartPanel"
|
||||
PanelKindTable PanelPluginKind = "signoz/TablePanel"
|
||||
@@ -176,7 +177,7 @@ const (
|
||||
)
|
||||
|
||||
func (PanelPluginKind) Enum() []any {
|
||||
return []any{PanelKindTimeSeries, PanelKindBarChart, PanelKindNumber, PanelKindPieChart, PanelKindTable, PanelKindHistogram, PanelKindList}
|
||||
return []any{PanelKindTimeSeries, PanelKindBarChart, PanelKindAreaChart, PanelKindNumber, PanelKindPieChart, PanelKindTable, PanelKindHistogram, PanelKindList}
|
||||
}
|
||||
|
||||
type TimeSeriesPanelSpec struct {
|
||||
@@ -204,6 +205,30 @@ type BarChartPanelSpec struct {
|
||||
Thresholds []ThresholdWithLabel `json:"thresholds" validate:"dive"`
|
||||
}
|
||||
|
||||
type AreaChartPanelSpec struct {
|
||||
Visualization AreaChartVisualization `json:"visualization"`
|
||||
Formatting PanelFormatting `json:"formatting"`
|
||||
ChartAppearance AreaChartAppearance `json:"chartAppearance"`
|
||||
Axes Axes `json:"axes"`
|
||||
Legend Legend `json:"legend"`
|
||||
Thresholds []ThresholdWithLabel `json:"thresholds" validate:"dive"`
|
||||
}
|
||||
|
||||
// AreaChartAppearance repeats the line-drawing fields rather than embedding
|
||||
// TimeSeriesChartAppearance: both carry a `fillMode` under different enums, and
|
||||
// a duplicated json tag across an embed boundary is resolved by depth, which the
|
||||
// schema reflector does not model.
|
||||
type AreaChartAppearance struct {
|
||||
LineInterpolation LineInterpolation `json:"lineInterpolation"`
|
||||
ShowPoints bool `json:"showPoints"`
|
||||
LineStyle LineStyle `json:"lineStyle"`
|
||||
FillMode AreaFillMode `json:"fillMode"`
|
||||
// FillOpacity is a pointer so an omitted field resolves to the kind default at
|
||||
// render time; a plain value would make the Go zero value a transparent fill.
|
||||
FillOpacity *FillOpacity `json:"fillOpacity"`
|
||||
SpanGaps SpanGaps `json:"spanGaps"`
|
||||
}
|
||||
|
||||
type NumberPanelSpec struct {
|
||||
Visualization BasicVisualization `json:"visualization"`
|
||||
Formatting PanelFormatting `json:"formatting"`
|
||||
@@ -262,6 +287,12 @@ type BarChartVisualization struct {
|
||||
StackedBarChart bool `json:"stackedBarChart"`
|
||||
}
|
||||
|
||||
type AreaChartVisualization struct {
|
||||
BasicVisualization
|
||||
FillSpans bool `json:"fillSpans"`
|
||||
Stack StackMode `json:"stack"`
|
||||
}
|
||||
|
||||
type PanelFormatting struct {
|
||||
Unit string `json:"unit"`
|
||||
DecimalPrecision PrecisionOption `json:"decimalPrecision"`
|
||||
@@ -622,6 +653,112 @@ func (fm *FillMode) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
}
|
||||
|
||||
// AreaFillMode is separate from FillMode so area panels can default to `solid`.
|
||||
// An area panel that omits the field would otherwise render as a bare line, and
|
||||
// flipping FillMode's default would change how existing TimeSeries panels render.
|
||||
type AreaFillMode struct{ valuer.String }
|
||||
|
||||
var (
|
||||
AreaFillModeSolid = AreaFillMode{valuer.NewString("solid")} // default
|
||||
AreaFillModeGradient = AreaFillMode{valuer.NewString("gradient")}
|
||||
// AreaFillModeNone lets an area panel degrade to a line without the save API
|
||||
// rejecting it, so an Area -> TimeSeries -> Area round trip keeps its config.
|
||||
AreaFillModeNone = AreaFillMode{valuer.NewString("none")}
|
||||
)
|
||||
|
||||
func (AreaFillMode) Enum() []any {
|
||||
return []any{AreaFillModeSolid, AreaFillModeGradient, AreaFillModeNone}
|
||||
}
|
||||
|
||||
func (fm AreaFillMode) ValueOrDefault() string {
|
||||
if fm.IsZero() {
|
||||
return AreaFillModeSolid.StringValue()
|
||||
}
|
||||
return fm.StringValue()
|
||||
}
|
||||
|
||||
func (fm AreaFillMode) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(fm.ValueOrDefault())
|
||||
}
|
||||
|
||||
func (fm *AreaFillMode) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid fill mode: must be a string, one of `solid`, `gradient`, or `none`")
|
||||
}
|
||||
val := AreaFillMode{valuer.NewString(v)}
|
||||
switch val {
|
||||
case AreaFillModeSolid, AreaFillModeGradient, AreaFillModeNone:
|
||||
*fm = val
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid fill mode %q: must be `solid`, `gradient`, or `none`", v)
|
||||
}
|
||||
}
|
||||
|
||||
// StackMode is area-only. Bar stacking stays on BarChartVisualization.StackedBarChart,
|
||||
// so `percent` is not reachable from a bar panel.
|
||||
type StackMode struct{ valuer.String }
|
||||
|
||||
var (
|
||||
StackModeNone = StackMode{valuer.NewString("none")} // default
|
||||
StackModeNormal = StackMode{valuer.NewString("normal")}
|
||||
StackModePercent = StackMode{valuer.NewString("percent")}
|
||||
)
|
||||
|
||||
func (StackMode) Enum() []any {
|
||||
return []any{StackModeNone, StackModeNormal, StackModePercent}
|
||||
}
|
||||
|
||||
func (sm StackMode) ValueOrDefault() string {
|
||||
if sm.IsZero() {
|
||||
return StackModeNone.StringValue()
|
||||
}
|
||||
return sm.StringValue()
|
||||
}
|
||||
|
||||
func (sm StackMode) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(sm.ValueOrDefault())
|
||||
}
|
||||
|
||||
func (sm *StackMode) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid stack mode: must be a string, one of `none`, `normal`, or `percent`")
|
||||
}
|
||||
val := StackMode{valuer.NewString(v)}
|
||||
switch val {
|
||||
case StackModeNone, StackModeNormal, StackModePercent:
|
||||
*sm = val
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid stack mode %q: must be `none`, `normal`, or `percent`", v)
|
||||
}
|
||||
}
|
||||
|
||||
// FillOpacity is the alpha of an area fill, in 0–1 because that is what the
|
||||
// chart layer consumes directly. Unlike the enums in this section it has no
|
||||
// ValueOrDefault: 0 is a legitimate value, so the kind default lives at render
|
||||
// time behind a nil pointer.
|
||||
type FillOpacity float64
|
||||
|
||||
func (FillOpacity) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
s.WithMinimum(0).WithMaximum(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *FillOpacity) UnmarshalJSON(data []byte) error {
|
||||
var v float64
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid fillOpacity: must be a number between 0 and 1")
|
||||
}
|
||||
if v < 0 || v > 1 {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid fillOpacity %v: must be between 0 and 1", v)
|
||||
}
|
||||
*o = FillOpacity(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
type SpanGaps struct {
|
||||
FillOnlyBelow bool `json:"fillOnlyBelow" description:"Controls whether lines connect across null values. When false (default), all gaps are connected. When true, only gaps smaller than fillLessThan are connected."`
|
||||
FillLessThan string `json:"fillLessThan" description:"The maximum gap size to connect when fillOnlyBelow is true. Gaps larger than this duration are left disconnected."`
|
||||
|
||||
@@ -14,6 +14,11 @@ import (
|
||||
// (transition.dashboardMigrateV5). Pre-v5 builder queries will produce
|
||||
// invalid v2 envelopes — run the v4→v5 migration first.
|
||||
//
|
||||
// The v1 input shape is closed: nothing writes v1 dashboards any more, so these
|
||||
// files only ever convert what v1 could already express. Panel kinds and spec
|
||||
// fields added to v2 from here on need no converter entry — change these files
|
||||
// only when a v2 type edit breaks the build.
|
||||
//
|
||||
// The conversion is split across sibling files by concern:
|
||||
// - perses_v1_to_v2_tags.go tags
|
||||
// - perses_v1_to_v2_panels.go widgets → panels (+ panel field mappers)
|
||||
|
||||
@@ -69,10 +69,6 @@ type PostableServiceAccount struct {
|
||||
Name string `json:"name" required:"true"`
|
||||
}
|
||||
|
||||
type DeprecatedPostableServiceAccountRole struct {
|
||||
ID valuer.UUID `json:"id" required:"true"`
|
||||
}
|
||||
|
||||
type PostableServiceAccountRole struct {
|
||||
ServiceAccountID valuer.UUID `json:"serviceAccountId" required:"true"`
|
||||
RoleID valuer.UUID `json:"roleId" required:"true"`
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"read",
|
||||
"update",
|
||||
"delete",
|
||||
"list"
|
||||
"list",
|
||||
"attach",
|
||||
"detach"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
511
tests/integration/tests/callbackauthn/05_authz.py
Normal file
511
tests/integration/tests/callbackauthn/05_authz.py
Normal file
@@ -0,0 +1,511 @@
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
|
||||
import requests
|
||||
from wiremock.resources.mappings import Mapping
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import (
|
||||
USER_ADMIN_EMAIL,
|
||||
USER_ADMIN_PASSWORD,
|
||||
add_license,
|
||||
change_user_role,
|
||||
create_active_user,
|
||||
)
|
||||
from fixtures.role import find_role_by_name, transaction_group
|
||||
|
||||
_EDITOR_EMAIL = "editor+authdomainauthz@integration.test"
|
||||
_EDITOR_PASSWORD = "password123Z$"
|
||||
_VIEWER_EMAIL = "viewer+authdomainauthz@integration.test"
|
||||
_VIEWER_PASSWORD = "password123Z$"
|
||||
|
||||
_ACTOR_ROLE_NAME = "auth-domain-fga-actor"
|
||||
_ACTOR_EMAIL = "customrole+authdomainauthz@integration.test"
|
||||
_ACTOR_PASSWORD = "password123Z$"
|
||||
|
||||
# Instance verbs are granted on _TARGET_A only; _TARGET_B must stay forbidden.
|
||||
_TARGET_A = "target-a-authdomain.integration.test"
|
||||
_TARGET_B = "target-b-authdomain.integration.test"
|
||||
_ADMIN_DOMAIN = "admin-crud-authdomain.integration.test"
|
||||
_ACTOR_DOMAIN = "actor-crud-authdomain.integration.test"
|
||||
|
||||
_SAML_CONFIG = {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_setup_managed_role_users_and_targets(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
for email, role, password, name in (
|
||||
(_EDITOR_EMAIL, "signoz-editor", _EDITOR_PASSWORD, "auth domain authz editor"),
|
||||
(_VIEWER_EMAIL, "signoz-viewer", _VIEWER_PASSWORD, "auth domain authz viewer"),
|
||||
):
|
||||
create_active_user(signoz, admin_token, email=email, role=role, password=password, name=name)
|
||||
|
||||
for name in (_TARGET_A, _TARGET_B):
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={"name": name, "enabled": True, "config": _SAML_CONFIG},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
|
||||
|
||||
def test_admin_can_crud(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={"name": _ADMIN_DOMAIN, "enabled": True, "config": _SAML_CONFIG},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
domain_id = response.json()["data"]["id"]
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert _ADMIN_DOMAIN in {domain["name"] for domain in response.json()["data"]}
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
json={"enabled": False, "config": _SAML_CONFIG},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
|
||||
def test_editor_and_viewer_forbidden(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
a_id = next(domain["id"] for domain in response.json()["data"] if domain["name"] == _TARGET_A)
|
||||
|
||||
for email, password in ((_EDITOR_EMAIL, _EDITOR_PASSWORD), (_VIEWER_EMAIL, _VIEWER_PASSWORD)):
|
||||
token = get_token(email, password)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, f"{email} list: expected 403, got {response.status_code}: {response.text}"
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={"name": _ACTOR_DOMAIN, "enabled": True, "config": _SAML_CONFIG},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, f"{email} create: expected 403, got {response.status_code}: {response.text}"
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{a_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, f"{email} get: expected 403, got {response.status_code}: {response.text}"
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{a_id}"),
|
||||
json={"enabled": True, "config": _SAML_CONFIG},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, f"{email} update: expected 403, got {response.status_code}: {response.text}"
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{a_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, f"{email} delete: expected 403, got {response.status_code}: {response.text}"
|
||||
|
||||
|
||||
def test_apply_license(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list[Mapping]], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
|
||||
def test_setup_actor(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_role: Callable[..., str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
create_role(admin_token, _ACTOR_ROLE_NAME)
|
||||
|
||||
user_id = create_active_user(
|
||||
signoz,
|
||||
admin_token,
|
||||
email=_ACTOR_EMAIL,
|
||||
role="signoz-viewer",
|
||||
password=_ACTOR_PASSWORD,
|
||||
name="auth-domain-fga-test-user",
|
||||
)
|
||||
change_user_role(signoz, admin_token, user_id, "signoz-viewer", _ACTOR_ROLE_NAME)
|
||||
|
||||
|
||||
def test_actor_without_grants_forbidden(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
token = get_token(_ACTOR_EMAIL, _ACTOR_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
a_id = next(domain["id"] for domain in response.json()["data"] if domain["name"] == _TARGET_A)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, f"list without grant: expected 403, got {response.status_code}: {response.text}"
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={"name": _ACTOR_DOMAIN, "enabled": True, "config": _SAML_CONFIG},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, f"create without grant: expected 403, got {response.status_code}: {response.text}"
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{a_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, f"read without grant: expected 403, got {response.status_code}: {response.text}"
|
||||
|
||||
|
||||
def test_create_requires_attach_on_mapped_roles(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
actor_id = find_role_by_name(signoz, admin_token, _ACTOR_ROLE_NAME)
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{actor_id}"),
|
||||
json={
|
||||
"description": "",
|
||||
"transactionGroups": [
|
||||
transaction_group("create", "metaresource", "auth-domain", ["*"]),
|
||||
transaction_group("delete", "metaresource", "auth-domain", ["*"]),
|
||||
transaction_group("list", "metaresource", "auth-domain", ["*"]),
|
||||
transaction_group("attach", "metaresource", "auth-domain", ["*"]),
|
||||
transaction_group("attach", "role", "role", ["signoz-viewer"]),
|
||||
],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
token = get_token(_ACTOR_EMAIL, _ACTOR_PASSWORD)
|
||||
|
||||
# No roleMapping means SSO users get signoz-viewer, which is granted.
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={"name": _ACTOR_DOMAIN, "enabled": True, "config": _SAML_CONFIG},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, f"create with default mapping: {response.text}"
|
||||
domain_id = response.json()["data"]["id"]
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": _ACTOR_DOMAIN,
|
||||
"enabled": True,
|
||||
"config": _SAML_CONFIG,
|
||||
"roleMapping": {"defaultRole": "EDITOR"},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, f"map to unattachable role: expected 403, got {response.status_code}: {response.text}"
|
||||
|
||||
# Trusting the IDP role attribute can grant any role, so it needs attach on role "*".
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": _ACTOR_DOMAIN,
|
||||
"enabled": True,
|
||||
"config": _SAML_CONFIG,
|
||||
"roleMapping": {"defaultRole": "VIEWER", "useRoleAttribute": True},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, f"role attribute without wildcard attach: expected 403, got {response.status_code}: {response.text}"
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{actor_id}"),
|
||||
json={
|
||||
"description": "",
|
||||
"transactionGroups": [
|
||||
transaction_group("create", "metaresource", "auth-domain", ["*"]),
|
||||
transaction_group("delete", "metaresource", "auth-domain", ["*"]),
|
||||
transaction_group("list", "metaresource", "auth-domain", ["*"]),
|
||||
transaction_group("attach", "metaresource", "auth-domain", ["*"]),
|
||||
transaction_group("attach", "role", "role", ["*"]),
|
||||
],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": _ACTOR_DOMAIN,
|
||||
"enabled": True,
|
||||
"config": _SAML_CONFIG,
|
||||
"roleMapping": {"defaultRole": "EDITOR", "groupMappings": {"platform-team": "ADMIN"}},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, f"create with wildcard role attach: {response.text}"
|
||||
domain_id = response.json()["data"]["id"]
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
|
||||
def test_update_requires_detach_on_stored_roles(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
actor_id = find_role_by_name(signoz, admin_token, _ACTOR_ROLE_NAME)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": _ACTOR_DOMAIN,
|
||||
"enabled": True,
|
||||
"config": _SAML_CONFIG,
|
||||
"roleMapping": {"defaultRole": "EDITOR"},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
domain_id = response.json()["data"]["id"]
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{actor_id}"),
|
||||
json={
|
||||
"description": "",
|
||||
"transactionGroups": [
|
||||
transaction_group("update", "metaresource", "auth-domain", [domain_id]),
|
||||
transaction_group("attach", "metaresource", "auth-domain", [domain_id]),
|
||||
transaction_group("detach", "metaresource", "auth-domain", [domain_id]),
|
||||
transaction_group("attach", "role", "role", ["signoz-viewer"]),
|
||||
transaction_group("detach", "role", "role", ["signoz-viewer"]),
|
||||
],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
token = get_token(_ACTOR_EMAIL, _ACTOR_PASSWORD)
|
||||
|
||||
# Dropping the mapping detaches the stored signoz-editor grant, which the
|
||||
# actor cannot detach.
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
json={"enabled": True, "config": _SAML_CONFIG},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, f"drop mapping without detach on stored role: expected 403, got {response.status_code}: {response.text}"
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{actor_id}"),
|
||||
json={
|
||||
"description": "",
|
||||
"transactionGroups": [
|
||||
transaction_group("update", "metaresource", "auth-domain", [domain_id]),
|
||||
transaction_group("attach", "metaresource", "auth-domain", [domain_id]),
|
||||
transaction_group("detach", "metaresource", "auth-domain", [domain_id]),
|
||||
transaction_group("attach", "role", "role", ["signoz-viewer"]),
|
||||
transaction_group("detach", "role", "role", ["signoz-editor"]),
|
||||
],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
json={"enabled": True, "config": _SAML_CONFIG},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, f"drop mapping with detach on stored role: {response.text}"
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
|
||||
def test_instance_verbs_scoped_to_granted_domain(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
actor_id = find_role_by_name(signoz, admin_token, _ACTOR_ROLE_NAME)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
ids = {domain["name"]: domain["id"] for domain in response.json()["data"]}
|
||||
a_id, b_id = ids[_TARGET_A], ids[_TARGET_B]
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{actor_id}"),
|
||||
json={
|
||||
"description": "",
|
||||
"transactionGroups": [
|
||||
transaction_group("read", "metaresource", "auth-domain", [a_id]),
|
||||
transaction_group("update", "metaresource", "auth-domain", [a_id]),
|
||||
transaction_group("delete", "metaresource", "auth-domain", [a_id]),
|
||||
transaction_group("attach", "metaresource", "auth-domain", [a_id]),
|
||||
transaction_group("detach", "metaresource", "auth-domain", [a_id]),
|
||||
transaction_group("attach", "role", "role", ["signoz-viewer"]),
|
||||
transaction_group("detach", "role", "role", ["signoz-viewer"]),
|
||||
],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
token = get_token(_ACTOR_EMAIL, _ACTOR_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{a_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, f"read granted domain: {response.text}"
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{b_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, f"read other domain: expected 403, got {response.status_code}: {response.text}"
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{a_id}"),
|
||||
json={"enabled": False, "config": _SAML_CONFIG},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, f"update granted domain: {response.text}"
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{b_id}"),
|
||||
json={"enabled": False, "config": _SAML_CONFIG},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, f"update other domain: expected 403, got {response.status_code}: {response.text}"
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{b_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, f"delete other domain: expected 403, got {response.status_code}: {response.text}"
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{a_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, f"delete granted domain: {response.text}"
|
||||
Reference in New Issue
Block a user