mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-20 11:40:27 +00:00
Compare commits
18 Commits
refactor/c
...
update-mem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9bcd2d4e1 | ||
|
|
34dcd79243 | ||
|
|
145d6327a7 | ||
|
|
61cfd33fc6 | ||
|
|
b299d63263 | ||
|
|
94f3e6d6d7 | ||
|
|
7ae0a23103 | ||
|
|
2d91b5fd0b | ||
|
|
ab1428d413 | ||
|
|
9c859e4d07 | ||
|
|
d6de4d58f7 | ||
|
|
e52c5683dd | ||
|
|
90e3cb6775 | ||
|
|
155f287462 | ||
|
|
c8fcc48022 | ||
|
|
44b6885639 | ||
|
|
0e5a128325 | ||
|
|
fd19ff8e5e |
@@ -190,7 +190,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:v0.116.0
|
image: signoz/signoz:v0.116.1
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080" # signoz port
|
- "8080:8080" # signoz port
|
||||||
# - "6060:6060" # pprof port
|
# - "6060:6060" # pprof port
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:v0.116.0
|
image: signoz/signoz:v0.116.1
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080" # signoz port
|
- "8080:8080" # signoz port
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:${VERSION:-v0.116.0}
|
image: signoz/signoz:${VERSION:-v0.116.1}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080" # signoz port
|
- "8080:8080" # signoz port
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:${VERSION:-v0.116.0}
|
image: signoz/signoz:${VERSION:-v0.116.1}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080" # signoz port
|
- "8080:8080" # signoz port
|
||||||
|
|||||||
@@ -2101,17 +2101,6 @@ components:
|
|||||||
role:
|
role:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
TypesPostableAcceptInvite:
|
|
||||||
properties:
|
|
||||||
displayName:
|
|
||||||
type: string
|
|
||||||
password:
|
|
||||||
type: string
|
|
||||||
sourceUrl:
|
|
||||||
type: string
|
|
||||||
token:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
TypesPostableBulkInviteRequest:
|
TypesPostableBulkInviteRequest:
|
||||||
properties:
|
properties:
|
||||||
invites:
|
invites:
|
||||||
@@ -3290,53 +3279,6 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- global
|
- global
|
||||||
/api/v1/invite:
|
/api/v1/invite:
|
||||||
get:
|
|
||||||
deprecated: false
|
|
||||||
description: This endpoint lists all invites
|
|
||||||
operationId: ListInvite
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/TypesInvite'
|
|
||||||
type: array
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- status
|
|
||||||
- data
|
|
||||||
type: object
|
|
||||||
description: OK
|
|
||||||
"401":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/RenderErrorResponse'
|
|
||||||
description: Unauthorized
|
|
||||||
"403":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/RenderErrorResponse'
|
|
||||||
description: Forbidden
|
|
||||||
"500":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/RenderErrorResponse'
|
|
||||||
description: Internal Server Error
|
|
||||||
security:
|
|
||||||
- api_key:
|
|
||||||
- ADMIN
|
|
||||||
- tokenizer:
|
|
||||||
- ADMIN
|
|
||||||
summary: List invites
|
|
||||||
tags:
|
|
||||||
- users
|
|
||||||
post:
|
post:
|
||||||
deprecated: false
|
deprecated: false
|
||||||
description: This endpoint creates an invite for a user
|
description: This endpoint creates an invite for a user
|
||||||
@@ -3399,151 +3341,6 @@ paths:
|
|||||||
summary: Create invite
|
summary: Create invite
|
||||||
tags:
|
tags:
|
||||||
- users
|
- users
|
||||||
/api/v1/invite/{id}:
|
|
||||||
delete:
|
|
||||||
deprecated: false
|
|
||||||
description: This endpoint deletes an invite by id
|
|
||||||
operationId: DeleteInvite
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
"204":
|
|
||||||
description: No Content
|
|
||||||
"400":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/RenderErrorResponse'
|
|
||||||
description: Bad Request
|
|
||||||
"401":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/RenderErrorResponse'
|
|
||||||
description: Unauthorized
|
|
||||||
"403":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/RenderErrorResponse'
|
|
||||||
description: Forbidden
|
|
||||||
"404":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/RenderErrorResponse'
|
|
||||||
description: Not Found
|
|
||||||
"500":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/RenderErrorResponse'
|
|
||||||
description: Internal Server Error
|
|
||||||
security:
|
|
||||||
- api_key:
|
|
||||||
- ADMIN
|
|
||||||
- tokenizer:
|
|
||||||
- ADMIN
|
|
||||||
summary: Delete invite
|
|
||||||
tags:
|
|
||||||
- users
|
|
||||||
/api/v1/invite/{token}:
|
|
||||||
get:
|
|
||||||
deprecated: false
|
|
||||||
description: This endpoint gets an invite by token
|
|
||||||
operationId: GetInvite
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: token
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
$ref: '#/components/schemas/TypesInvite'
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- status
|
|
||||||
- data
|
|
||||||
type: object
|
|
||||||
description: OK
|
|
||||||
"400":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/RenderErrorResponse'
|
|
||||||
description: Bad Request
|
|
||||||
"404":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/RenderErrorResponse'
|
|
||||||
description: Not Found
|
|
||||||
"500":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/RenderErrorResponse'
|
|
||||||
description: Internal Server Error
|
|
||||||
summary: Get invite
|
|
||||||
tags:
|
|
||||||
- users
|
|
||||||
/api/v1/invite/accept:
|
|
||||||
post:
|
|
||||||
deprecated: false
|
|
||||||
description: This endpoint accepts an invite by token
|
|
||||||
operationId: AcceptInvite
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/TypesPostableAcceptInvite'
|
|
||||||
responses:
|
|
||||||
"201":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
$ref: '#/components/schemas/TypesUser'
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- status
|
|
||||||
- data
|
|
||||||
type: object
|
|
||||||
description: Created
|
|
||||||
"400":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/RenderErrorResponse'
|
|
||||||
description: Bad Request
|
|
||||||
"404":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/RenderErrorResponse'
|
|
||||||
description: Not Found
|
|
||||||
"500":
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/RenderErrorResponse'
|
|
||||||
description: Internal Server Error
|
|
||||||
summary: Accept invite
|
|
||||||
tags:
|
|
||||||
- users
|
|
||||||
/api/v1/invite/bulk:
|
/api/v1/invite/bulk:
|
||||||
post:
|
post:
|
||||||
deprecated: false
|
deprecated: false
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ const config: Config.InitialOptions = {
|
|||||||
'<rootDir>/node_modules/@signozhq/icons/dist/index.esm.js',
|
'<rootDir>/node_modules/@signozhq/icons/dist/index.esm.js',
|
||||||
'^react-syntax-highlighter/dist/esm/(.*)$':
|
'^react-syntax-highlighter/dist/esm/(.*)$':
|
||||||
'<rootDir>/node_modules/react-syntax-highlighter/dist/cjs/$1',
|
'<rootDir>/node_modules/react-syntax-highlighter/dist/cjs/$1',
|
||||||
'^@signozhq/([^/]+)$': '<rootDir>/node_modules/@signozhq/$1/dist/$1.js',
|
'^@signozhq/(?!ui$)([^/]+)$':
|
||||||
|
'<rootDir>/node_modules/@signozhq/$1/dist/$1.js',
|
||||||
},
|
},
|
||||||
extensionsToTreatAsEsm: ['.ts'],
|
extensionsToTreatAsEsm: ['.ts'],
|
||||||
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
|
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
"@signozhq/table": "0.3.7",
|
"@signozhq/table": "0.3.7",
|
||||||
"@signozhq/toggle-group": "0.0.1",
|
"@signozhq/toggle-group": "0.0.1",
|
||||||
"@signozhq/tooltip": "0.0.2",
|
"@signozhq/tooltip": "0.0.2",
|
||||||
|
"@signozhq/ui": "0.0.5",
|
||||||
"@tanstack/react-table": "8.20.6",
|
"@tanstack/react-table": "8.20.6",
|
||||||
"@tanstack/react-virtual": "3.11.2",
|
"@tanstack/react-virtual": "3.11.2",
|
||||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||||
|
|||||||
@@ -2511,25 +2511,6 @@ export interface TypesPostableAPIKeyDTO {
|
|||||||
role?: string;
|
role?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TypesPostableAcceptInviteDTO {
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
displayName?: string;
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
password?: string;
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
sourceUrl?: string;
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
token?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TypesPostableBulkInviteRequestDTO {
|
export interface TypesPostableBulkInviteRequestDTO {
|
||||||
/**
|
/**
|
||||||
* @type array
|
* @type array
|
||||||
@@ -3033,17 +3014,6 @@ export type GetGlobalConfig200 = {
|
|||||||
status: string;
|
status: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ListInvite200 = {
|
|
||||||
/**
|
|
||||||
* @type array
|
|
||||||
*/
|
|
||||||
data: TypesInviteDTO[];
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CreateInvite201 = {
|
export type CreateInvite201 = {
|
||||||
data: TypesInviteDTO;
|
data: TypesInviteDTO;
|
||||||
/**
|
/**
|
||||||
@@ -3052,28 +3022,6 @@ export type CreateInvite201 = {
|
|||||||
status: string;
|
status: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeleteInvitePathParameters = {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
export type GetInvitePathParameters = {
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
export type GetInvite200 = {
|
|
||||||
data: TypesInviteDTO;
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AcceptInvite201 = {
|
|
||||||
data: TypesUserDTO;
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ListPromotedAndIndexedPaths200 = {
|
export type ListPromotedAndIndexedPaths200 = {
|
||||||
/**
|
/**
|
||||||
* @type array
|
* @type array
|
||||||
|
|||||||
@@ -20,26 +20,20 @@ import { useMutation, useQuery } from 'react-query';
|
|||||||
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
|
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
|
||||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||||
import type {
|
import type {
|
||||||
AcceptInvite201,
|
|
||||||
ChangePasswordPathParameters,
|
ChangePasswordPathParameters,
|
||||||
CreateAPIKey201,
|
CreateAPIKey201,
|
||||||
CreateInvite201,
|
CreateInvite201,
|
||||||
DeleteInvitePathParameters,
|
|
||||||
DeleteUserPathParameters,
|
DeleteUserPathParameters,
|
||||||
GetInvite200,
|
|
||||||
GetInvitePathParameters,
|
|
||||||
GetMyUser200,
|
GetMyUser200,
|
||||||
GetResetPasswordToken200,
|
GetResetPasswordToken200,
|
||||||
GetResetPasswordTokenPathParameters,
|
GetResetPasswordTokenPathParameters,
|
||||||
GetUser200,
|
GetUser200,
|
||||||
GetUserPathParameters,
|
GetUserPathParameters,
|
||||||
ListAPIKeys200,
|
ListAPIKeys200,
|
||||||
ListInvite200,
|
|
||||||
ListUsers200,
|
ListUsers200,
|
||||||
RenderErrorResponseDTO,
|
RenderErrorResponseDTO,
|
||||||
RevokeAPIKeyPathParameters,
|
RevokeAPIKeyPathParameters,
|
||||||
TypesChangePasswordRequestDTO,
|
TypesChangePasswordRequestDTO,
|
||||||
TypesPostableAcceptInviteDTO,
|
|
||||||
TypesPostableAPIKeyDTO,
|
TypesPostableAPIKeyDTO,
|
||||||
TypesPostableBulkInviteRequestDTO,
|
TypesPostableBulkInviteRequestDTO,
|
||||||
TypesPostableForgotPasswordDTO,
|
TypesPostableForgotPasswordDTO,
|
||||||
@@ -255,84 +249,6 @@ export const invalidateGetResetPasswordToken = async (
|
|||||||
return queryClient;
|
return queryClient;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* This endpoint lists all invites
|
|
||||||
* @summary List invites
|
|
||||||
*/
|
|
||||||
export const listInvite = (signal?: AbortSignal) => {
|
|
||||||
return GeneratedAPIInstance<ListInvite200>({
|
|
||||||
url: `/api/v1/invite`,
|
|
||||||
method: 'GET',
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getListInviteQueryKey = () => {
|
|
||||||
return [`/api/v1/invite`] as const;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getListInviteQueryOptions = <
|
|
||||||
TData = Awaited<ReturnType<typeof listInvite>>,
|
|
||||||
TError = ErrorType<RenderErrorResponseDTO>
|
|
||||||
>(options?: {
|
|
||||||
query?: UseQueryOptions<Awaited<ReturnType<typeof listInvite>>, TError, TData>;
|
|
||||||
}) => {
|
|
||||||
const { query: queryOptions } = options ?? {};
|
|
||||||
|
|
||||||
const queryKey = queryOptions?.queryKey ?? getListInviteQueryKey();
|
|
||||||
|
|
||||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listInvite>>> = ({
|
|
||||||
signal,
|
|
||||||
}) => listInvite(signal);
|
|
||||||
|
|
||||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
|
||||||
Awaited<ReturnType<typeof listInvite>>,
|
|
||||||
TError,
|
|
||||||
TData
|
|
||||||
> & { queryKey: QueryKey };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ListInviteQueryResult = NonNullable<
|
|
||||||
Awaited<ReturnType<typeof listInvite>>
|
|
||||||
>;
|
|
||||||
export type ListInviteQueryError = ErrorType<RenderErrorResponseDTO>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary List invites
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function useListInvite<
|
|
||||||
TData = Awaited<ReturnType<typeof listInvite>>,
|
|
||||||
TError = ErrorType<RenderErrorResponseDTO>
|
|
||||||
>(options?: {
|
|
||||||
query?: UseQueryOptions<Awaited<ReturnType<typeof listInvite>>, TError, TData>;
|
|
||||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
|
||||||
const queryOptions = getListInviteQueryOptions(options);
|
|
||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
|
||||||
queryKey: QueryKey;
|
|
||||||
};
|
|
||||||
|
|
||||||
query.queryKey = queryOptions.queryKey;
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary List invites
|
|
||||||
*/
|
|
||||||
export const invalidateListInvite = async (
|
|
||||||
queryClient: QueryClient,
|
|
||||||
options?: InvalidateOptions,
|
|
||||||
): Promise<QueryClient> => {
|
|
||||||
await queryClient.invalidateQueries(
|
|
||||||
{ queryKey: getListInviteQueryKey() },
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
return queryClient;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This endpoint creates an invite for a user
|
* This endpoint creates an invite for a user
|
||||||
* @summary Create invite
|
* @summary Create invite
|
||||||
@@ -416,257 +332,6 @@ export const useCreateInvite = <
|
|||||||
|
|
||||||
return useMutation(mutationOptions);
|
return useMutation(mutationOptions);
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* This endpoint deletes an invite by id
|
|
||||||
* @summary Delete invite
|
|
||||||
*/
|
|
||||||
export const deleteInvite = ({ id }: DeleteInvitePathParameters) => {
|
|
||||||
return GeneratedAPIInstance<void>({
|
|
||||||
url: `/api/v1/invite/${id}`,
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDeleteInviteMutationOptions = <
|
|
||||||
TError = ErrorType<RenderErrorResponseDTO>,
|
|
||||||
TContext = unknown
|
|
||||||
>(options?: {
|
|
||||||
mutation?: UseMutationOptions<
|
|
||||||
Awaited<ReturnType<typeof deleteInvite>>,
|
|
||||||
TError,
|
|
||||||
{ pathParams: DeleteInvitePathParameters },
|
|
||||||
TContext
|
|
||||||
>;
|
|
||||||
}): UseMutationOptions<
|
|
||||||
Awaited<ReturnType<typeof deleteInvite>>,
|
|
||||||
TError,
|
|
||||||
{ pathParams: DeleteInvitePathParameters },
|
|
||||||
TContext
|
|
||||||
> => {
|
|
||||||
const mutationKey = ['deleteInvite'];
|
|
||||||
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 deleteInvite>>,
|
|
||||||
{ pathParams: DeleteInvitePathParameters }
|
|
||||||
> = (props) => {
|
|
||||||
const { pathParams } = props ?? {};
|
|
||||||
|
|
||||||
return deleteInvite(pathParams);
|
|
||||||
};
|
|
||||||
|
|
||||||
return { mutationFn, ...mutationOptions };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DeleteInviteMutationResult = NonNullable<
|
|
||||||
Awaited<ReturnType<typeof deleteInvite>>
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type DeleteInviteMutationError = ErrorType<RenderErrorResponseDTO>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Delete invite
|
|
||||||
*/
|
|
||||||
export const useDeleteInvite = <
|
|
||||||
TError = ErrorType<RenderErrorResponseDTO>,
|
|
||||||
TContext = unknown
|
|
||||||
>(options?: {
|
|
||||||
mutation?: UseMutationOptions<
|
|
||||||
Awaited<ReturnType<typeof deleteInvite>>,
|
|
||||||
TError,
|
|
||||||
{ pathParams: DeleteInvitePathParameters },
|
|
||||||
TContext
|
|
||||||
>;
|
|
||||||
}): UseMutationResult<
|
|
||||||
Awaited<ReturnType<typeof deleteInvite>>,
|
|
||||||
TError,
|
|
||||||
{ pathParams: DeleteInvitePathParameters },
|
|
||||||
TContext
|
|
||||||
> => {
|
|
||||||
const mutationOptions = getDeleteInviteMutationOptions(options);
|
|
||||||
|
|
||||||
return useMutation(mutationOptions);
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* This endpoint gets an invite by token
|
|
||||||
* @summary Get invite
|
|
||||||
*/
|
|
||||||
export const getInvite = (
|
|
||||||
{ token }: GetInvitePathParameters,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
) => {
|
|
||||||
return GeneratedAPIInstance<GetInvite200>({
|
|
||||||
url: `/api/v1/invite/${token}`,
|
|
||||||
method: 'GET',
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGetInviteQueryKey = ({ token }: GetInvitePathParameters) => {
|
|
||||||
return [`/api/v1/invite/${token}`] as const;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGetInviteQueryOptions = <
|
|
||||||
TData = Awaited<ReturnType<typeof getInvite>>,
|
|
||||||
TError = ErrorType<RenderErrorResponseDTO>
|
|
||||||
>(
|
|
||||||
{ token }: GetInvitePathParameters,
|
|
||||||
options?: {
|
|
||||||
query?: UseQueryOptions<Awaited<ReturnType<typeof getInvite>>, TError, TData>;
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
const { query: queryOptions } = options ?? {};
|
|
||||||
|
|
||||||
const queryKey = queryOptions?.queryKey ?? getGetInviteQueryKey({ token });
|
|
||||||
|
|
||||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getInvite>>> = ({
|
|
||||||
signal,
|
|
||||||
}) => getInvite({ token }, signal);
|
|
||||||
|
|
||||||
return {
|
|
||||||
queryKey,
|
|
||||||
queryFn,
|
|
||||||
enabled: !!token,
|
|
||||||
...queryOptions,
|
|
||||||
} as UseQueryOptions<Awaited<ReturnType<typeof getInvite>>, TError, TData> & {
|
|
||||||
queryKey: QueryKey;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetInviteQueryResult = NonNullable<
|
|
||||||
Awaited<ReturnType<typeof getInvite>>
|
|
||||||
>;
|
|
||||||
export type GetInviteQueryError = ErrorType<RenderErrorResponseDTO>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get invite
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function useGetInvite<
|
|
||||||
TData = Awaited<ReturnType<typeof getInvite>>,
|
|
||||||
TError = ErrorType<RenderErrorResponseDTO>
|
|
||||||
>(
|
|
||||||
{ token }: GetInvitePathParameters,
|
|
||||||
options?: {
|
|
||||||
query?: UseQueryOptions<Awaited<ReturnType<typeof getInvite>>, TError, TData>;
|
|
||||||
},
|
|
||||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
|
||||||
const queryOptions = getGetInviteQueryOptions({ token }, options);
|
|
||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
|
||||||
queryKey: QueryKey;
|
|
||||||
};
|
|
||||||
|
|
||||||
query.queryKey = queryOptions.queryKey;
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get invite
|
|
||||||
*/
|
|
||||||
export const invalidateGetInvite = async (
|
|
||||||
queryClient: QueryClient,
|
|
||||||
{ token }: GetInvitePathParameters,
|
|
||||||
options?: InvalidateOptions,
|
|
||||||
): Promise<QueryClient> => {
|
|
||||||
await queryClient.invalidateQueries(
|
|
||||||
{ queryKey: getGetInviteQueryKey({ token }) },
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
return queryClient;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This endpoint accepts an invite by token
|
|
||||||
* @summary Accept invite
|
|
||||||
*/
|
|
||||||
export const acceptInvite = (
|
|
||||||
typesPostableAcceptInviteDTO: BodyType<TypesPostableAcceptInviteDTO>,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
) => {
|
|
||||||
return GeneratedAPIInstance<AcceptInvite201>({
|
|
||||||
url: `/api/v1/invite/accept`,
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
data: typesPostableAcceptInviteDTO,
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAcceptInviteMutationOptions = <
|
|
||||||
TError = ErrorType<RenderErrorResponseDTO>,
|
|
||||||
TContext = unknown
|
|
||||||
>(options?: {
|
|
||||||
mutation?: UseMutationOptions<
|
|
||||||
Awaited<ReturnType<typeof acceptInvite>>,
|
|
||||||
TError,
|
|
||||||
{ data: BodyType<TypesPostableAcceptInviteDTO> },
|
|
||||||
TContext
|
|
||||||
>;
|
|
||||||
}): UseMutationOptions<
|
|
||||||
Awaited<ReturnType<typeof acceptInvite>>,
|
|
||||||
TError,
|
|
||||||
{ data: BodyType<TypesPostableAcceptInviteDTO> },
|
|
||||||
TContext
|
|
||||||
> => {
|
|
||||||
const mutationKey = ['acceptInvite'];
|
|
||||||
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 acceptInvite>>,
|
|
||||||
{ data: BodyType<TypesPostableAcceptInviteDTO> }
|
|
||||||
> = (props) => {
|
|
||||||
const { data } = props ?? {};
|
|
||||||
|
|
||||||
return acceptInvite(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
return { mutationFn, ...mutationOptions };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AcceptInviteMutationResult = NonNullable<
|
|
||||||
Awaited<ReturnType<typeof acceptInvite>>
|
|
||||||
>;
|
|
||||||
export type AcceptInviteMutationBody = BodyType<TypesPostableAcceptInviteDTO>;
|
|
||||||
export type AcceptInviteMutationError = ErrorType<RenderErrorResponseDTO>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Accept invite
|
|
||||||
*/
|
|
||||||
export const useAcceptInvite = <
|
|
||||||
TError = ErrorType<RenderErrorResponseDTO>,
|
|
||||||
TContext = unknown
|
|
||||||
>(options?: {
|
|
||||||
mutation?: UseMutationOptions<
|
|
||||||
Awaited<ReturnType<typeof acceptInvite>>,
|
|
||||||
TError,
|
|
||||||
{ data: BodyType<TypesPostableAcceptInviteDTO> },
|
|
||||||
TContext
|
|
||||||
>;
|
|
||||||
}): UseMutationResult<
|
|
||||||
Awaited<ReturnType<typeof acceptInvite>>,
|
|
||||||
TError,
|
|
||||||
{ data: BodyType<TypesPostableAcceptInviteDTO> },
|
|
||||||
TContext
|
|
||||||
> => {
|
|
||||||
const mutationOptions = getAcceptInviteMutationOptions(options);
|
|
||||||
|
|
||||||
return useMutation(mutationOptions);
|
|
||||||
};
|
|
||||||
/**
|
/**
|
||||||
* This endpoint creates a bulk invite for a user
|
* This endpoint creates a bulk invite for a user
|
||||||
* @summary Create bulk invite
|
* @summary Create bulk invite
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import axios from 'api';
|
|
||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
|
||||||
import { PayloadProps, PendingInvite } from 'types/api/user/getPendingInvites';
|
|
||||||
|
|
||||||
const get = async (): Promise<SuccessResponseV2<PendingInvite[]>> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get<PayloadProps>(`/invite`);
|
|
||||||
return {
|
|
||||||
httpStatusCode: response.status,
|
|
||||||
data: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default get;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import axios from 'api';
|
|
||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
|
||||||
import { PayloadProps, Props } from 'types/api/user/accept';
|
|
||||||
import { UserResponse } from 'types/api/user/getUser';
|
|
||||||
|
|
||||||
const accept = async (
|
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponseV2<UserResponse>> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post<PayloadProps>(`/invite/accept`, props);
|
|
||||||
return {
|
|
||||||
httpStatusCode: response.status,
|
|
||||||
data: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default accept;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import axios from 'api';
|
|
||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
|
||||||
import { Props } from 'types/api/user/deleteInvite';
|
|
||||||
|
|
||||||
const del = async (props: Props): Promise<SuccessResponseV2<null>> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.delete(`/invite/${props.id}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
httpStatusCode: response.status,
|
|
||||||
data: null,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default del;
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import axios from 'api';
|
|
||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
|
||||||
import {
|
|
||||||
InviteDetails,
|
|
||||||
PayloadProps,
|
|
||||||
Props,
|
|
||||||
} from 'types/api/user/getInviteDetails';
|
|
||||||
|
|
||||||
const getInviteDetails = async (
|
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponseV2<InviteDetails>> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get<PayloadProps>(
|
|
||||||
`/invite/${props.inviteId}?ref=${window.location.href}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
httpStatusCode: response.status,
|
|
||||||
data: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getInviteDetails;
|
|
||||||
1
frontend/src/auto-import-registry.d.ts
vendored
1
frontend/src/auto-import-registry.d.ts
vendored
@@ -30,3 +30,4 @@ import '@signozhq/switch';
|
|||||||
import '@signozhq/table';
|
import '@signozhq/table';
|
||||||
import '@signozhq/toggle-group';
|
import '@signozhq/toggle-group';
|
||||||
import '@signozhq/tooltip';
|
import '@signozhq/tooltip';
|
||||||
|
import '@signozhq/ui';
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
Check,
|
Check,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Copy,
|
Copy,
|
||||||
Link,
|
|
||||||
LockKeyhole,
|
LockKeyhole,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Trash2,
|
Trash2,
|
||||||
@@ -17,14 +16,11 @@ import { Input } from '@signozhq/input';
|
|||||||
import { toast } from '@signozhq/sonner';
|
import { toast } from '@signozhq/sonner';
|
||||||
import { Select } from 'antd';
|
import { Select } from 'antd';
|
||||||
import getResetPasswordToken from 'api/v1/factor_password/getResetPasswordToken';
|
import getResetPasswordToken from 'api/v1/factor_password/getResetPasswordToken';
|
||||||
import sendInvite from 'api/v1/invite/create';
|
|
||||||
import cancelInvite from 'api/v1/invite/id/delete';
|
|
||||||
import deleteUser from 'api/v1/user/id/delete';
|
import deleteUser from 'api/v1/user/id/delete';
|
||||||
import update from 'api/v1/user/id/update';
|
import update from 'api/v1/user/id/update';
|
||||||
import { MemberRow } from 'components/MembersTable/MembersTable';
|
import { MemberRow } from 'components/MembersTable/MembersTable';
|
||||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||||
import ROUTES from 'constants/routes';
|
import { MemberStatus } from 'container/MembersSettings/utils';
|
||||||
import { INVITE_PREFIX, MemberStatus } from 'container/MembersSettings/utils';
|
|
||||||
import { capitalize } from 'lodash-es';
|
import { capitalize } from 'lodash-es';
|
||||||
import { useTimezone } from 'providers/Timezone';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { ROLES } from 'types/roles';
|
import { ROLES } from 'types/roles';
|
||||||
@@ -36,7 +32,6 @@ export interface EditMemberDrawerProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
onRefetch?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
@@ -45,7 +40,6 @@ function EditMemberDrawer({
|
|||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
onComplete,
|
onComplete,
|
||||||
onRefetch,
|
|
||||||
}: EditMemberDrawerProps): JSX.Element {
|
}: EditMemberDrawerProps): JSX.Element {
|
||||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
@@ -58,11 +52,9 @@ function EditMemberDrawer({
|
|||||||
const [resetLink, setResetLink] = useState<string | null>(null);
|
const [resetLink, setResetLink] = useState<string | null>(null);
|
||||||
const [showResetLinkDialog, setShowResetLinkDialog] = useState(false);
|
const [showResetLinkDialog, setShowResetLinkDialog] = useState(false);
|
||||||
const [hasCopiedResetLink, setHasCopiedResetLink] = useState(false);
|
const [hasCopiedResetLink, setHasCopiedResetLink] = useState(false);
|
||||||
|
const [linkType, setLinkType] = useState<'invite' | 'reset' | null>(null);
|
||||||
|
|
||||||
const isInvited = member?.status === MemberStatus.Invited;
|
const isInvited = member?.status === MemberStatus.Invited;
|
||||||
// Invited member IDs are prefixed with 'invite-'; strip it to get the real invite ID
|
|
||||||
const inviteId =
|
|
||||||
isInvited && member ? member.id.slice(INVITE_PREFIX.length) : null;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (member) {
|
if (member) {
|
||||||
@@ -73,7 +65,7 @@ function EditMemberDrawer({
|
|||||||
|
|
||||||
const isDirty =
|
const isDirty =
|
||||||
member !== null &&
|
member !== null &&
|
||||||
(displayName !== member.name || selectedRole !== member.role);
|
(displayName !== (member.name ?? '') || selectedRole !== member.role);
|
||||||
|
|
||||||
const formatTimestamp = useCallback(
|
const formatTimestamp = useCallback(
|
||||||
(ts: string | null | undefined): string => {
|
(ts: string | null | undefined): string => {
|
||||||
@@ -89,80 +81,22 @@ function EditMemberDrawer({
|
|||||||
[formatTimezoneAdjustedTimestamp],
|
[formatTimezoneAdjustedTimestamp],
|
||||||
);
|
);
|
||||||
|
|
||||||
const saveInvitedMember = useCallback(async (): Promise<void> => {
|
|
||||||
if (!member || !inviteId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await cancelInvite({ id: inviteId });
|
|
||||||
try {
|
|
||||||
await sendInvite({
|
|
||||||
email: member.email,
|
|
||||||
name: displayName,
|
|
||||||
role: selectedRole,
|
|
||||||
frontendBaseUrl: window.location.origin,
|
|
||||||
});
|
|
||||||
toast.success('Invite updated successfully', { richColors: true });
|
|
||||||
onComplete();
|
|
||||||
onClose();
|
|
||||||
} catch {
|
|
||||||
onRefetch?.();
|
|
||||||
onClose();
|
|
||||||
toast.error(
|
|
||||||
'Failed to send the updated invite. Please re-invite this member.',
|
|
||||||
{ richColors: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
member,
|
|
||||||
inviteId,
|
|
||||||
displayName,
|
|
||||||
selectedRole,
|
|
||||||
onComplete,
|
|
||||||
onClose,
|
|
||||||
onRefetch,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const saveActiveMember = useCallback(async (): Promise<void> => {
|
|
||||||
if (!member) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await update({
|
|
||||||
userId: member.id,
|
|
||||||
displayName,
|
|
||||||
role: selectedRole,
|
|
||||||
});
|
|
||||||
toast.success('Member details updated successfully', { richColors: true });
|
|
||||||
onComplete();
|
|
||||||
onClose();
|
|
||||||
}, [member, displayName, selectedRole, onComplete, onClose]);
|
|
||||||
|
|
||||||
const handleSave = useCallback(async (): Promise<void> => {
|
const handleSave = useCallback(async (): Promise<void> => {
|
||||||
if (!member || !isDirty) {
|
if (!member || !isDirty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
if (isInvited && inviteId) {
|
await update({ userId: member.id, displayName, role: selectedRole });
|
||||||
await saveInvitedMember();
|
toast.success('Member details updated successfully', { richColors: true });
|
||||||
} else {
|
onComplete();
|
||||||
await saveActiveMember();
|
onClose();
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(
|
toast.error('Failed to update member details', { richColors: true });
|
||||||
isInvited ? 'Failed to update invite' : 'Failed to update member details',
|
|
||||||
{ richColors: true },
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
}, [
|
}, [member, isDirty, displayName, selectedRole, onComplete, onClose]);
|
||||||
member,
|
|
||||||
isDirty,
|
|
||||||
isInvited,
|
|
||||||
inviteId,
|
|
||||||
saveInvitedMember,
|
|
||||||
saveActiveMember,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleDelete = useCallback(async (): Promise<void> => {
|
const handleDelete = useCallback(async (): Promise<void> => {
|
||||||
if (!member) {
|
if (!member) {
|
||||||
@@ -170,25 +104,23 @@ function EditMemberDrawer({
|
|||||||
}
|
}
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
try {
|
try {
|
||||||
if (isInvited && inviteId) {
|
await deleteUser({ userId: member.id });
|
||||||
await cancelInvite({ id: inviteId });
|
toast.success(
|
||||||
toast.success('Invitation cancelled successfully', { richColors: true });
|
isInvited ? 'Invite revoked successfully' : 'Member deleted successfully',
|
||||||
} else {
|
{ richColors: true },
|
||||||
await deleteUser({ userId: member.id });
|
);
|
||||||
toast.success('Member deleted successfully', { richColors: true });
|
|
||||||
}
|
|
||||||
setShowDeleteConfirm(false);
|
setShowDeleteConfirm(false);
|
||||||
onComplete();
|
onComplete();
|
||||||
onClose();
|
onClose();
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(
|
toast.error(
|
||||||
isInvited ? 'Failed to cancel invitation' : 'Failed to delete member',
|
isInvited ? 'Failed to revoke invite' : 'Failed to delete member',
|
||||||
{ richColors: true },
|
{ richColors: true },
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
}
|
}
|
||||||
}, [member, isInvited, inviteId, onComplete, onClose]);
|
}, [member, isInvited, onComplete, onClose]);
|
||||||
|
|
||||||
const handleGenerateResetLink = useCallback(async (): Promise<void> => {
|
const handleGenerateResetLink = useCallback(async (): Promise<void> => {
|
||||||
if (!member) {
|
if (!member) {
|
||||||
@@ -201,6 +133,7 @@ function EditMemberDrawer({
|
|||||||
const link = `${window.location.origin}/password-reset?token=${response.data.token}`;
|
const link = `${window.location.origin}/password-reset?token=${response.data.token}`;
|
||||||
setResetLink(link);
|
setResetLink(link);
|
||||||
setHasCopiedResetLink(false);
|
setHasCopiedResetLink(false);
|
||||||
|
setLinkType(isInvited ? 'invite' : 'reset');
|
||||||
setShowResetLinkDialog(true);
|
setShowResetLinkDialog(true);
|
||||||
onClose();
|
onClose();
|
||||||
} else {
|
} else {
|
||||||
@@ -217,7 +150,7 @@ function EditMemberDrawer({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsGeneratingLink(false);
|
setIsGeneratingLink(false);
|
||||||
}
|
}
|
||||||
}, [member, onClose]);
|
}, [member, isInvited, setLinkType, onClose]);
|
||||||
|
|
||||||
const handleCopyResetLink = useCallback(async (): Promise<void> => {
|
const handleCopyResetLink = useCallback(async (): Promise<void> => {
|
||||||
if (!resetLink) {
|
if (!resetLink) {
|
||||||
@@ -227,36 +160,18 @@ function EditMemberDrawer({
|
|||||||
await navigator.clipboard.writeText(resetLink);
|
await navigator.clipboard.writeText(resetLink);
|
||||||
setHasCopiedResetLink(true);
|
setHasCopiedResetLink(true);
|
||||||
setTimeout(() => setHasCopiedResetLink(false), 2000);
|
setTimeout(() => setHasCopiedResetLink(false), 2000);
|
||||||
toast.success('Reset link copied to clipboard', { richColors: true });
|
toast.success(
|
||||||
|
linkType === 'invite'
|
||||||
|
? 'Invite link copied to clipboard'
|
||||||
|
: 'Reset link copied to clipboard',
|
||||||
|
{ richColors: true },
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to copy link', {
|
toast.error('Failed to copy link', {
|
||||||
richColors: true,
|
richColors: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [resetLink]);
|
}, [resetLink, linkType]);
|
||||||
|
|
||||||
const handleCopyInviteLink = useCallback(async (): Promise<void> => {
|
|
||||||
if (!member?.token) {
|
|
||||||
toast.error('Invite link is not available', {
|
|
||||||
richColors: true,
|
|
||||||
position: 'top-right',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const inviteLink = `${window.location.origin}${ROUTES.SIGN_UP}?token=${member.token}`;
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(inviteLink);
|
|
||||||
toast.success('Invite link copied to clipboard', {
|
|
||||||
richColors: true,
|
|
||||||
position: 'top-right',
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
toast.error('Failed to copy invite link', {
|
|
||||||
richColors: true,
|
|
||||||
position: 'top-right',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [member]);
|
|
||||||
|
|
||||||
const handleClose = useCallback((): void => {
|
const handleClose = useCallback((): void => {
|
||||||
setShowDeleteConfirm(false);
|
setShowDeleteConfirm(false);
|
||||||
@@ -348,30 +263,22 @@ function EditMemberDrawer({
|
|||||||
onClick={(): void => setShowDeleteConfirm(true)}
|
onClick={(): void => setShowDeleteConfirm(true)}
|
||||||
>
|
>
|
||||||
<Trash2 size={12} />
|
<Trash2 size={12} />
|
||||||
{isInvited ? 'Cancel Invite' : 'Delete Member'}
|
{isInvited ? 'Revoke Invite' : 'Delete Member'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="edit-member-drawer__footer-divider" />
|
<div className="edit-member-drawer__footer-divider" />
|
||||||
|
<Button
|
||||||
{isInvited ? (
|
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--warning"
|
||||||
<Button
|
onClick={handleGenerateResetLink}
|
||||||
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--warning"
|
disabled={isGeneratingLink}
|
||||||
onClick={handleCopyInviteLink}
|
>
|
||||||
disabled={!member?.token}
|
<RefreshCw size={12} />
|
||||||
>
|
{isGeneratingLink
|
||||||
<Link size={12} />
|
? 'Generating...'
|
||||||
Copy Invite Link
|
: isInvited
|
||||||
</Button>
|
? 'Copy Invite Link'
|
||||||
) : (
|
: 'Generate Password Reset Link'}
|
||||||
<Button
|
</Button>
|
||||||
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--warning"
|
|
||||||
onClick={handleGenerateResetLink}
|
|
||||||
disabled={isGeneratingLink}
|
|
||||||
>
|
|
||||||
<RefreshCw size={12} />
|
|
||||||
{isGeneratingLink ? 'Generating...' : 'Generate Password Reset Link'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="edit-member-drawer__footer-right">
|
<div className="edit-member-drawer__footer-right">
|
||||||
@@ -394,21 +301,21 @@ function EditMemberDrawer({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteDialogTitle = isInvited ? 'Cancel Invitation' : 'Delete Member';
|
const deleteDialogTitle = isInvited ? 'Revoke Invite' : 'Delete Member';
|
||||||
const deleteDialogBody = isInvited ? (
|
const deleteDialogBody = isInvited ? (
|
||||||
<>
|
<>
|
||||||
Are you sure you want to cancel the invitation for{' '}
|
Are you sure you want to revoke the invite for{' '}
|
||||||
<strong>{member?.email}</strong>? They will no longer be able to join the
|
<strong>{member?.email}</strong>? They will no longer be able to join the
|
||||||
workspace using this invite.
|
workspace using this invite.
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
Are you sure you want to delete{' '}
|
Are you sure you want to delete{' '}
|
||||||
<strong>{member?.name || member?.email}</strong>? This will permanently
|
<strong>{member?.name || member?.email}</strong>? This will remove their
|
||||||
remove their access to the workspace.
|
access to the workspace.
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
const deleteConfirmLabel = isInvited ? 'Cancel Invite' : 'Delete Member';
|
const deleteConfirmLabel = isInvited ? 'Revoke Invite' : 'Delete Member';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -434,17 +341,19 @@ function EditMemberDrawer({
|
|||||||
onOpenChange={(isOpen): void => {
|
onOpenChange={(isOpen): void => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
setShowResetLinkDialog(false);
|
setShowResetLinkDialog(false);
|
||||||
|
setLinkType(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title="Password Reset Link"
|
title={linkType === 'invite' ? 'Invite Link' : 'Password Reset Link'}
|
||||||
showCloseButton
|
showCloseButton
|
||||||
width="base"
|
width="base"
|
||||||
className="reset-link-dialog"
|
className="reset-link-dialog"
|
||||||
>
|
>
|
||||||
<div className="reset-link-dialog__content">
|
<div className="reset-link-dialog__content">
|
||||||
<p className="reset-link-dialog__description">
|
<p className="reset-link-dialog__description">
|
||||||
This creates a one-time link the team member can use to set a new password
|
{linkType === 'invite'
|
||||||
for their SigNoz account.
|
? 'Share this one-time link with the team member to complete their account setup.'
|
||||||
|
: 'This creates a one-time link the team member can use to set a new password for their SigNoz account.'}
|
||||||
</p>
|
</p>
|
||||||
<div className="reset-link-dialog__link-row">
|
<div className="reset-link-dialog__link-row">
|
||||||
<div className="reset-link-dialog__link-text-wrap">
|
<div className="reset-link-dialog__link-text-wrap">
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { toast } from '@signozhq/sonner';
|
import { toast } from '@signozhq/sonner';
|
||||||
import getResetPasswordToken from 'api/v1/factor_password/getResetPasswordToken';
|
import getResetPasswordToken from 'api/v1/factor_password/getResetPasswordToken';
|
||||||
import cancelInvite from 'api/v1/invite/id/delete';
|
|
||||||
import deleteUser from 'api/v1/user/id/delete';
|
import deleteUser from 'api/v1/user/id/delete';
|
||||||
import update from 'api/v1/user/id/update';
|
import update from 'api/v1/user/id/update';
|
||||||
import { MemberStatus } from 'container/MembersSettings/utils';
|
import { MemberStatus } from 'container/MembersSettings/utils';
|
||||||
@@ -48,8 +47,6 @@ jest.mock('@signozhq/dialog', () => ({
|
|||||||
|
|
||||||
jest.mock('api/v1/user/id/update');
|
jest.mock('api/v1/user/id/update');
|
||||||
jest.mock('api/v1/user/id/delete');
|
jest.mock('api/v1/user/id/delete');
|
||||||
jest.mock('api/v1/invite/id/delete');
|
|
||||||
jest.mock('api/v1/invite/create');
|
|
||||||
jest.mock('api/v1/factor_password/getResetPasswordToken');
|
jest.mock('api/v1/factor_password/getResetPasswordToken');
|
||||||
jest.mock('@signozhq/sonner', () => ({
|
jest.mock('@signozhq/sonner', () => ({
|
||||||
toast: {
|
toast: {
|
||||||
@@ -60,7 +57,6 @@ jest.mock('@signozhq/sonner', () => ({
|
|||||||
|
|
||||||
const mockUpdate = jest.mocked(update);
|
const mockUpdate = jest.mocked(update);
|
||||||
const mockDeleteUser = jest.mocked(deleteUser);
|
const mockDeleteUser = jest.mocked(deleteUser);
|
||||||
const mockCancelInvite = jest.mocked(cancelInvite);
|
|
||||||
const mockGetResetPasswordToken = jest.mocked(getResetPasswordToken);
|
const mockGetResetPasswordToken = jest.mocked(getResetPasswordToken);
|
||||||
|
|
||||||
const activeMember = {
|
const activeMember = {
|
||||||
@@ -74,13 +70,12 @@ const activeMember = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const invitedMember = {
|
const invitedMember = {
|
||||||
id: 'invite-abc123',
|
id: 'abc123',
|
||||||
name: '',
|
name: '',
|
||||||
email: 'bob@signoz.io',
|
email: 'bob@signoz.io',
|
||||||
role: 'VIEWER' as ROLES,
|
role: 'VIEWER' as ROLES,
|
||||||
status: MemberStatus.Invited,
|
status: MemberStatus.Invited,
|
||||||
joinedOn: '1700000000000',
|
joinedOn: '1700000000000',
|
||||||
token: 'tok-xyz',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderDrawer(
|
function renderDrawer(
|
||||||
@@ -102,7 +97,6 @@ describe('EditMemberDrawer', () => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
mockUpdate.mockResolvedValue({ httpStatusCode: 200, data: null });
|
mockUpdate.mockResolvedValue({ httpStatusCode: 200, data: null });
|
||||||
mockDeleteUser.mockResolvedValue({ httpStatusCode: 200, data: null });
|
mockDeleteUser.mockResolvedValue({ httpStatusCode: 200, data: null });
|
||||||
mockCancelInvite.mockResolvedValue({ httpStatusCode: 200, data: null });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders active member details and disables Save when form is not dirty', () => {
|
it('renders active member details and disables Save when form is not dirty', () => {
|
||||||
@@ -163,36 +157,61 @@ describe('EditMemberDrawer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows Cancel Invite and Copy Invite Link for invited members; hides Last Modified', () => {
|
it('shows revoke invite and copy invite link for invited members; hides Last Modified', () => {
|
||||||
renderDrawer({ member: invitedMember });
|
renderDrawer({ member: invitedMember });
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('button', { name: /cancel invite/i }),
|
screen.getByRole('button', { name: /revoke invite/i }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('button', { name: /copy invite link/i }),
|
screen.getByRole('button', { name: /copy invite link/i }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByRole('button', { name: /generate password reset link/i }),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
expect(screen.getByText('Invited On')).toBeInTheDocument();
|
expect(screen.getByText('Invited On')).toBeInTheDocument();
|
||||||
expect(screen.queryByText('Last Modified')).not.toBeInTheDocument();
|
expect(screen.queryByText('Last Modified')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls cancelInvite after confirming Cancel Invite for invited members', async () => {
|
it('calls deleteUser after confirming revoke invite for invited members', async () => {
|
||||||
const onComplete = jest.fn();
|
const onComplete = jest.fn();
|
||||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
renderDrawer({ member: invitedMember, onComplete });
|
renderDrawer({ member: invitedMember, onComplete });
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /cancel invite/i }));
|
await user.click(screen.getByRole('button', { name: /revoke invite/i }));
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await screen.findByText(/are you sure you want to cancel the invitation/i),
|
await screen.findByText(/Are you sure you want to revoke the invite/i),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
const confirmBtns = screen.getAllByRole('button', { name: /cancel invite/i });
|
const confirmBtns = screen.getAllByRole('button', { name: /revoke invite/i });
|
||||||
await user.click(confirmBtns[confirmBtns.length - 1]);
|
await user.click(confirmBtns[confirmBtns.length - 1]);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockCancelInvite).toHaveBeenCalledWith({ id: 'abc123' });
|
expect(mockDeleteUser).toHaveBeenCalledWith({ userId: 'abc123' });
|
||||||
|
expect(onComplete).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls update API when saving changes for an invited member', async () => {
|
||||||
|
const onComplete = jest.fn();
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
renderDrawer({ member: { ...invitedMember, name: 'Bob' }, onComplete });
|
||||||
|
|
||||||
|
const nameInput = screen.getByDisplayValue('Bob');
|
||||||
|
await user.clear(nameInput);
|
||||||
|
await user.type(nameInput, 'Bob Updated');
|
||||||
|
|
||||||
|
const saveBtn = screen.getByRole('button', { name: /save member details/i });
|
||||||
|
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||||||
|
await user.click(saveBtn);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockUpdate).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ userId: 'abc123', displayName: 'Bob Updated' }),
|
||||||
|
);
|
||||||
expect(onComplete).toHaveBeenCalled();
|
expect(onComplete).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -260,7 +279,6 @@ describe('EditMemberDrawer', () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /^copy$/i }));
|
fireEvent.click(screen.getByRole('button', { name: /^copy$/i }));
|
||||||
|
|
||||||
// Verify success path: writeText called with the correct link
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockToast.success).toHaveBeenCalledWith(
|
expect(mockToast.success).toHaveBeenCalledWith(
|
||||||
'Reset link copied to clipboard',
|
'Reset link copied to clipboard',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { Badge } from '@signozhq/badge';
|
import { Badge } from '@signozhq/badge';
|
||||||
import { Pagination, Table, Tooltip } from 'antd';
|
import { Table, Tooltip } from 'antd';
|
||||||
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
|
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
|
||||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||||
import { MemberStatus } from 'container/MembersSettings/utils';
|
import { MemberStatus } from 'container/MembersSettings/utils';
|
||||||
@@ -18,7 +18,6 @@ export interface MemberRow {
|
|||||||
status: MemberStatus;
|
status: MemberStatus;
|
||||||
joinedOn: string | null;
|
joinedOn: string | null;
|
||||||
updatedAt?: string | null;
|
updatedAt?: string | null;
|
||||||
token?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MembersTableProps {
|
interface MembersTableProps {
|
||||||
@@ -64,11 +63,23 @@ function StatusBadge({ status }: { status: MemberRow['status'] }): JSX.Element {
|
|||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
if (status === MemberStatus.Deleted) {
|
||||||
<Badge color="amber" variant="outline">
|
return (
|
||||||
INVITED
|
<Badge color="cherry" variant="outline">
|
||||||
</Badge>
|
DELETED
|
||||||
);
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === MemberStatus.Invited) {
|
||||||
|
return (
|
||||||
|
<Badge color="amber" variant="outline">
|
||||||
|
INVITED
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Badge color="vanilla">⎯</Badge>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MembersEmptyState({
|
function MembersEmptyState({
|
||||||
@@ -199,14 +210,30 @@ function MembersTable({
|
|||||||
dataSource={data}
|
dataSource={data}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={false}
|
pagination={{
|
||||||
|
current: currentPage,
|
||||||
|
pageSize,
|
||||||
|
total,
|
||||||
|
showTotal: showPaginationTotal,
|
||||||
|
showSizeChanger: false,
|
||||||
|
onChange: onPageChange,
|
||||||
|
className: 'members-table-pagination',
|
||||||
|
hideOnSinglePage: true,
|
||||||
|
}}
|
||||||
rowClassName={(_, index): string =>
|
rowClassName={(_, index): string =>
|
||||||
index % 2 === 0 ? 'members-table-row--tinted' : ''
|
index % 2 === 0 ? 'members-table-row--tinted' : ''
|
||||||
}
|
}
|
||||||
onRow={(record): React.HTMLAttributes<HTMLElement> => ({
|
onRow={(record): React.HTMLAttributes<HTMLElement> => {
|
||||||
onClick: (): void => onRowClick?.(record),
|
const isClickable = onRowClick && record.status !== MemberStatus.Deleted;
|
||||||
style: onRowClick ? { cursor: 'pointer' } : undefined,
|
return {
|
||||||
})}
|
onClick: (): void => {
|
||||||
|
if (isClickable) {
|
||||||
|
onRowClick(record);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: isClickable ? { cursor: 'pointer' } : undefined,
|
||||||
|
};
|
||||||
|
}}
|
||||||
onChange={(_, __, sorter): void => {
|
onChange={(_, __, sorter): void => {
|
||||||
if (onSortChange) {
|
if (onSortChange) {
|
||||||
onSortChange(
|
onSortChange(
|
||||||
@@ -220,17 +247,6 @@ function MembersTable({
|
|||||||
}}
|
}}
|
||||||
className="members-table"
|
className="members-table"
|
||||||
/>
|
/>
|
||||||
{total > pageSize && (
|
|
||||||
<Pagination
|
|
||||||
current={currentPage}
|
|
||||||
pageSize={pageSize}
|
|
||||||
total={total}
|
|
||||||
showTotal={showPaginationTotal}
|
|
||||||
showSizeChanger={false}
|
|
||||||
onChange={onPageChange}
|
|
||||||
className="members-table-pagination"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,12 @@ const mockActiveMembers: MemberRow[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const mockInvitedMember: MemberRow = {
|
const mockInvitedMember: MemberRow = {
|
||||||
id: 'invite-abc',
|
id: 'inv-abc',
|
||||||
name: '',
|
name: '',
|
||||||
email: 'charlie@signoz.io',
|
email: 'charlie@signoz.io',
|
||||||
role: 'EDITOR' as ROLES,
|
role: 'EDITOR' as ROLES,
|
||||||
status: MemberStatus.Invited,
|
status: MemberStatus.Invited,
|
||||||
joinedOn: null,
|
joinedOn: null,
|
||||||
token: 'tok-123',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
@@ -93,6 +92,34 @@ describe('MembersTable', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders DELETED badge and does not call onRowClick when a deleted member row is clicked', async () => {
|
||||||
|
const onRowClick = jest.fn();
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
const deletedMember: MemberRow = {
|
||||||
|
id: 'user-del',
|
||||||
|
name: 'Dave Deleted',
|
||||||
|
email: 'dave@signoz.io',
|
||||||
|
role: 'VIEWER' as ROLES,
|
||||||
|
status: MemberStatus.Deleted,
|
||||||
|
joinedOn: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MembersTable
|
||||||
|
{...defaultProps}
|
||||||
|
data={[...mockActiveMembers, deletedMember]}
|
||||||
|
total={3}
|
||||||
|
onRowClick={onRowClick}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('DELETED')).toBeInTheDocument();
|
||||||
|
await user.click(screen.getByText('Dave Deleted'));
|
||||||
|
expect(onRowClick).not.toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ id: 'user-del' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('shows "No members found" empty state when no data and no search query', () => {
|
it('shows "No members found" empty state when no data and no search query', () => {
|
||||||
render(<MembersTable {...defaultProps} data={[]} total={0} searchQuery="" />);
|
render(<MembersTable {...defaultProps} data={[]} total={0} searchQuery="" />);
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,14 @@ import { UseQueryResult } from 'react-query';
|
|||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button, Card, Skeleton, Typography } from 'antd';
|
import { Button, Card, Skeleton, Typography } from 'antd';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { useGetGraphCustomSeries } from 'components/CeleryTask/useGetGraphCustomSeries';
|
|
||||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||||
import Uplot from 'components/Uplot';
|
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
|
||||||
import {
|
import {
|
||||||
getCustomFiltersForBarChart,
|
getCustomFiltersForBarChart,
|
||||||
getFormattedEndPointStatusCodeChartData,
|
getFormattedEndPointStatusCodeChartData,
|
||||||
getStatusCodeBarChartWidgetData,
|
getStatusCodeBarChartWidgetData,
|
||||||
statusCodeWidgetInfo,
|
statusCodeWidgetInfo,
|
||||||
} from 'container/ApiMonitoring/utils';
|
} from 'container/ApiMonitoring/utils';
|
||||||
|
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||||
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
|
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
|
||||||
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
|
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
|
||||||
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
|
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||||
@@ -20,15 +18,16 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useResizeObserver } from 'hooks/useDimensions';
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
|
||||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
|
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||||
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { Options } from 'uplot';
|
|
||||||
|
|
||||||
import ErrorState from './ErrorState';
|
import ErrorState from './ErrorState';
|
||||||
|
import { prepareStatusCodeBarChartsConfig } from './utils';
|
||||||
|
|
||||||
function StatusCodeBarCharts({
|
function StatusCodeBarCharts({
|
||||||
endPointStatusCodeBarChartsDataQuery,
|
endPointStatusCodeBarChartsDataQuery,
|
||||||
@@ -67,13 +66,6 @@ function StatusCodeBarCharts({
|
|||||||
} = endPointStatusCodeLatencyBarChartsDataQuery;
|
} = endPointStatusCodeLatencyBarChartsDataQuery;
|
||||||
|
|
||||||
const { startTime: minTime, endTime: maxTime } = timeRange;
|
const { startTime: minTime, endTime: maxTime } = timeRange;
|
||||||
const legendScrollPositionRef = useRef<{
|
|
||||||
scrollTop: number;
|
|
||||||
scrollLeft: number;
|
|
||||||
}>({
|
|
||||||
scrollTop: 0,
|
|
||||||
scrollLeft: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const graphRef = useRef<HTMLDivElement>(null);
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
const dimensions = useResizeObserver(graphRef);
|
const dimensions = useResizeObserver(graphRef);
|
||||||
@@ -119,6 +111,7 @@ function StatusCodeBarCharts({
|
|||||||
|
|
||||||
const navigateToExplorer = useNavigateToExplorer();
|
const navigateToExplorer = useNavigateToExplorer();
|
||||||
const { currentQuery } = useQueryBuilder();
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const navigateToExplorerPages = useNavigateToExplorerPages();
|
const navigateToExplorerPages = useNavigateToExplorerPages();
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
@@ -134,12 +127,6 @@ function StatusCodeBarCharts({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getCustomSeries } = useGetGraphCustomSeries({
|
|
||||||
isDarkMode,
|
|
||||||
drawStyle: 'bars',
|
|
||||||
colorMapping,
|
|
||||||
});
|
|
||||||
|
|
||||||
const widget = useMemo<Widgets>(
|
const widget = useMemo<Widgets>(
|
||||||
() =>
|
() =>
|
||||||
getStatusCodeBarChartWidgetData(domainName, {
|
getStatusCodeBarChartWidgetData(domainName, {
|
||||||
@@ -193,49 +180,36 @@ function StatusCodeBarCharts({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const options = useMemo(
|
const config = useMemo(() => {
|
||||||
() =>
|
const apiResponse =
|
||||||
getUPlotChartOptions({
|
currentWidgetInfoIndex === 0
|
||||||
apiResponse:
|
? formattedEndPointStatusCodeBarChartsDataPayload
|
||||||
currentWidgetInfoIndex === 0
|
: formattedEndPointStatusCodeLatencyBarChartsDataPayload;
|
||||||
? formattedEndPointStatusCodeBarChartsDataPayload
|
return prepareStatusCodeBarChartsConfig({
|
||||||
: formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
timezone,
|
||||||
isDarkMode,
|
|
||||||
dimensions,
|
|
||||||
yAxisUnit: statusCodeWidgetInfo[currentWidgetInfoIndex].yAxisUnit,
|
|
||||||
softMax: null,
|
|
||||||
softMin: null,
|
|
||||||
minTimeScale: minTime,
|
|
||||||
maxTimeScale: maxTime,
|
|
||||||
panelType: PANEL_TYPES.BAR,
|
|
||||||
onClickHandler: graphClickHandler,
|
|
||||||
customSeries: getCustomSeries,
|
|
||||||
onDragSelect,
|
|
||||||
colorMapping,
|
|
||||||
query: currentQuery,
|
|
||||||
legendScrollPosition: legendScrollPositionRef.current,
|
|
||||||
setLegendScrollPosition: (position: {
|
|
||||||
scrollTop: number;
|
|
||||||
scrollLeft: number;
|
|
||||||
}) => {
|
|
||||||
legendScrollPositionRef.current = position;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
minTime,
|
|
||||||
maxTime,
|
|
||||||
currentWidgetInfoIndex,
|
|
||||||
dimensions,
|
|
||||||
formattedEndPointStatusCodeBarChartsDataPayload,
|
|
||||||
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
graphClickHandler,
|
query: currentQuery,
|
||||||
getCustomSeries,
|
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
|
onClick: graphClickHandler,
|
||||||
|
apiResponse,
|
||||||
|
minTimeScale: minTime,
|
||||||
|
maxTimeScale: maxTime,
|
||||||
|
yAxisUnit: statusCodeWidgetInfo[currentWidgetInfoIndex].yAxisUnit,
|
||||||
colorMapping,
|
colorMapping,
|
||||||
currentQuery,
|
});
|
||||||
],
|
}, [
|
||||||
);
|
currentQuery,
|
||||||
|
isDarkMode,
|
||||||
|
minTime,
|
||||||
|
maxTime,
|
||||||
|
graphClickHandler,
|
||||||
|
onDragSelect,
|
||||||
|
formattedEndPointStatusCodeBarChartsDataPayload,
|
||||||
|
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
||||||
|
timezone,
|
||||||
|
currentWidgetInfoIndex,
|
||||||
|
colorMapping,
|
||||||
|
]);
|
||||||
|
|
||||||
const renderCardContent = useCallback(
|
const renderCardContent = useCallback(
|
||||||
(query: UseQueryResult<SuccessResponse<any>, unknown>): JSX.Element => {
|
(query: UseQueryResult<SuccessResponse<any>, unknown>): JSX.Element => {
|
||||||
@@ -253,11 +227,20 @@ function StatusCodeBarCharts({
|
|||||||
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Uplot options={options as Options} data={chartData} />
|
<BarChart
|
||||||
|
config={config}
|
||||||
|
data={chartData}
|
||||||
|
width={dimensions.width}
|
||||||
|
height={dimensions.height}
|
||||||
|
timezone={timezone}
|
||||||
|
legendConfig={{
|
||||||
|
position: LegendPosition.BOTTOM,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[options, chartData],
|
[config, chartData, dimensions, timezone],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { ExecStats } from 'api/v5/v5';
|
||||||
|
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||||
|
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||||
|
import getLabelName from 'lib/getLabelName';
|
||||||
|
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
|
import { DrawStyle } from 'lib/uPlotV2/config/types';
|
||||||
|
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||||
|
import { get } from 'lodash-es';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { QueryData } from 'types/api/widgets/getQuery';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
export const prepareStatusCodeBarChartsConfig = ({
|
||||||
|
timezone,
|
||||||
|
isDarkMode,
|
||||||
|
query,
|
||||||
|
onDragSelect,
|
||||||
|
onClick,
|
||||||
|
apiResponse,
|
||||||
|
minTimeScale,
|
||||||
|
maxTimeScale,
|
||||||
|
yAxisUnit,
|
||||||
|
colorMapping,
|
||||||
|
}: {
|
||||||
|
timezone: Timezone;
|
||||||
|
isDarkMode: boolean;
|
||||||
|
query: Query;
|
||||||
|
onDragSelect: (startTime: number, endTime: number) => void;
|
||||||
|
onClick?: OnClickPluginOpts['onClick'];
|
||||||
|
minTimeScale?: number;
|
||||||
|
maxTimeScale?: number;
|
||||||
|
apiResponse: MetricRangePayloadProps;
|
||||||
|
yAxisUnit?: string;
|
||||||
|
colorMapping?: Record<string, string>;
|
||||||
|
}): UPlotConfigBuilder => {
|
||||||
|
const stepIntervals: ExecStats['stepIntervals'] = get(
|
||||||
|
apiResponse,
|
||||||
|
'data.newResult.meta.stepIntervals',
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
const minStepInterval = Math.min(...Object.values(stepIntervals));
|
||||||
|
|
||||||
|
const config = buildBaseConfig({
|
||||||
|
id: v4(),
|
||||||
|
yAxisUnit: yAxisUnit,
|
||||||
|
apiResponse,
|
||||||
|
isDarkMode,
|
||||||
|
onDragSelect,
|
||||||
|
timezone,
|
||||||
|
onClick,
|
||||||
|
minTimeScale,
|
||||||
|
maxTimeScale,
|
||||||
|
stepInterval: minStepInterval,
|
||||||
|
panelType: PANEL_TYPES.BAR,
|
||||||
|
});
|
||||||
|
|
||||||
|
const seriesList: QueryData[] = apiResponse?.data?.result || [];
|
||||||
|
seriesList.forEach((series) => {
|
||||||
|
const baseLabelName = getLabelName(
|
||||||
|
series.metric,
|
||||||
|
series.queryName || '', // query
|
||||||
|
series.legend || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const label = query ? getLegend(series, query, baseLabelName) : baseLabelName;
|
||||||
|
|
||||||
|
const currentStepInterval = get(stepIntervals, series.queryName, undefined);
|
||||||
|
|
||||||
|
config.addSeries({
|
||||||
|
scaleKey: 'y',
|
||||||
|
drawStyle: DrawStyle.Bar,
|
||||||
|
label: label,
|
||||||
|
colorMapping: colorMapping ?? {},
|
||||||
|
isDarkMode,
|
||||||
|
stepInterval: currentStepInterval,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
||||||
@@ -21,10 +21,15 @@ interface MockQueryResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mocks
|
// Mocks
|
||||||
jest.mock('components/Uplot', () => ({
|
jest.mock(
|
||||||
__esModule: true,
|
'container/DashboardContainer/visualization/charts/BarChart/BarChart',
|
||||||
default: jest.fn().mockImplementation(() => <div data-testid="uplot-mock" />),
|
() => ({
|
||||||
}));
|
__esModule: true,
|
||||||
|
default: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() => <div data-testid="bar-chart-mock" />),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
jest.mock('components/CeleryTask/useGetGraphCustomSeries', () => ({
|
jest.mock('components/CeleryTask/useGetGraphCustomSeries', () => ({
|
||||||
useGetGraphCustomSeries: (): { getCustomSeries: jest.Mock } => ({
|
useGetGraphCustomSeries: (): { getCustomSeries: jest.Mock } => ({
|
||||||
@@ -70,6 +75,24 @@ jest.mock('hooks/useNotifications', () => ({
|
|||||||
useNotifications: (): { notifications: [] } => ({ notifications: [] }),
|
useNotifications: (): { notifications: [] } => ({ notifications: [] }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('providers/Timezone', () => ({
|
||||||
|
useTimezone: (): {
|
||||||
|
timezone: {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
offset: string;
|
||||||
|
searchIndex: string;
|
||||||
|
};
|
||||||
|
} => ({
|
||||||
|
timezone: {
|
||||||
|
name: 'UTC',
|
||||||
|
value: 'UTC',
|
||||||
|
offset: '+00:00',
|
||||||
|
searchIndex: 'UTC',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('lib/uPlotLib/getUplotChartOptions', () => ({
|
jest.mock('lib/uPlotLib/getUplotChartOptions', () => ({
|
||||||
getUPlotChartOptions: jest.fn().mockReturnValue({}),
|
getUPlotChartOptions: jest.fn().mockReturnValue({}),
|
||||||
}));
|
}));
|
||||||
@@ -319,7 +342,7 @@ describe('StatusCodeBarCharts', () => {
|
|||||||
mockData.payload,
|
mockData.payload,
|
||||||
'sum',
|
'sum',
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId('uplot-mock')).toBeInTheDocument();
|
expect(screen.getByTestId('bar-chart-mock')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Number of calls')).toBeInTheDocument();
|
expect(screen.getByText('Number of calls')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Latency')).toBeInTheDocument();
|
expect(screen.getByText('Latency')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -337,31 +337,6 @@
|
|||||||
|
|
||||||
.login-submit-btn {
|
.login-submit-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 32px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
background: var(--primary);
|
|
||||||
border: none;
|
|
||||||
border-radius: 2px;
|
|
||||||
font-family: Inter, sans-serif;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1;
|
|
||||||
color: var(--bg-neutral-dark-50);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
|
||||||
background: var(--primary);
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background: var(--primary);
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { Button } from '@signozhq/button';
|
import { Button } from '@signozhq/ui';
|
||||||
import { Form, Input, Select, Typography } from 'antd';
|
import { Form, Input, Select, Typography } from 'antd';
|
||||||
import getVersion from 'api/v1/version/get';
|
import getVersion from 'api/v1/version/get';
|
||||||
import get from 'api/v2/sessions/context/get';
|
import get from 'api/v2/sessions/context/get';
|
||||||
@@ -392,9 +392,9 @@ function Login(): JSX.Element {
|
|||||||
disabled={!isNextButtonEnabled}
|
disabled={!isNextButtonEnabled}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
onClick={onNextHandler}
|
onClick={onNextHandler}
|
||||||
data-testid="initiate_login"
|
testId="initiate_login"
|
||||||
className="login-submit-btn"
|
className="login-submit-btn"
|
||||||
suffixIcon={<ArrowRight size={12} />}
|
suffix={<ArrowRight />}
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
@@ -406,10 +406,10 @@ function Login(): JSX.Element {
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
type="submit"
|
type="submit"
|
||||||
color="primary"
|
color="primary"
|
||||||
data-testid="callback_authn_submit"
|
testId="callback_authn_submit"
|
||||||
data-attr="signup"
|
data-attr="signup"
|
||||||
className="login-submit-btn"
|
className="login-submit-btn"
|
||||||
suffixIcon={<ArrowRight size={12} />}
|
suffix={<ArrowRight />}
|
||||||
>
|
>
|
||||||
Sign in with SSO
|
Sign in with SSO
|
||||||
</Button>
|
</Button>
|
||||||
@@ -420,11 +420,11 @@ function Login(): JSX.Element {
|
|||||||
disabled={!isSubmitButtonEnabled}
|
disabled={!isSubmitButtonEnabled}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="primary"
|
color="primary"
|
||||||
data-testid="password_authn_submit"
|
testId="password_authn_submit"
|
||||||
type="submit"
|
type="submit"
|
||||||
data-attr="signup"
|
data-attr="signup"
|
||||||
className="login-submit-btn"
|
className="login-submit-btn"
|
||||||
suffixIcon={<ArrowRight size={12} />}
|
suffix={<ArrowRight />}
|
||||||
>
|
>
|
||||||
Sign in with Password
|
Sign in with Password
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Check, ChevronDown, Plus } from '@signozhq/icons';
|
|||||||
import { Input } from '@signozhq/input';
|
import { Input } from '@signozhq/input';
|
||||||
import type { MenuProps } from 'antd';
|
import type { MenuProps } from 'antd';
|
||||||
import { Dropdown } from 'antd';
|
import { Dropdown } from 'antd';
|
||||||
import getPendingInvites from 'api/v1/invite/get';
|
|
||||||
import getAll from 'api/v1/user/get';
|
import getAll from 'api/v1/user/get';
|
||||||
import EditMemberDrawer from 'components/EditMemberDrawer/EditMemberDrawer';
|
import EditMemberDrawer from 'components/EditMemberDrawer/EditMemberDrawer';
|
||||||
import InviteMembersModal from 'components/InviteMembersModal/InviteMembersModal';
|
import InviteMembersModal from 'components/InviteMembersModal/InviteMembersModal';
|
||||||
@@ -14,7 +13,7 @@ import MembersTable, { MemberRow } from 'components/MembersTable/MembersTable';
|
|||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
|
|
||||||
import { FilterMode, INVITE_PREFIX, MemberStatus } from './utils';
|
import { FilterMode, MemberStatus, toMemberStatus } from './utils';
|
||||||
|
|
||||||
import './MembersSettings.styles.scss';
|
import './MembersSettings.styles.scss';
|
||||||
|
|
||||||
@@ -34,51 +33,24 @@ function MembersSettings(): JSX.Element {
|
|||||||
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
|
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
|
||||||
const [selectedMember, setSelectedMember] = useState<MemberRow | null>(null);
|
const [selectedMember, setSelectedMember] = useState<MemberRow | null>(null);
|
||||||
|
|
||||||
const {
|
const { data: usersData, isLoading, refetch: refetchUsers } = useQuery({
|
||||||
data: usersData,
|
|
||||||
isLoading: isUsersLoading,
|
|
||||||
refetch: refetchUsers,
|
|
||||||
} = useQuery({
|
|
||||||
queryFn: getAll,
|
queryFn: getAll,
|
||||||
queryKey: ['getOrgUser', org?.[0]?.id],
|
queryKey: ['getOrgUser', org?.[0]?.id],
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const allMembers = useMemo(
|
||||||
data: invitesData,
|
(): MemberRow[] =>
|
||||||
isLoading: isInvitesLoading,
|
(usersData?.data ?? []).map((user) => ({
|
||||||
refetch: refetchInvites,
|
id: user.id,
|
||||||
} = useQuery({
|
name: user.displayName,
|
||||||
queryFn: getPendingInvites,
|
email: user.email,
|
||||||
queryKey: ['getPendingInvites'],
|
role: user.role,
|
||||||
});
|
status: toMemberStatus(user.status ?? ''),
|
||||||
|
joinedOn: user.createdAt ? String(user.createdAt) : null,
|
||||||
const isLoading = isUsersLoading || isInvitesLoading;
|
updatedAt: user.updatedAt ? String(user.updatedAt) : null,
|
||||||
|
})),
|
||||||
const allMembers = useMemo((): MemberRow[] => {
|
[usersData],
|
||||||
const activeMembers: MemberRow[] = (usersData?.data ?? []).map((user) => ({
|
);
|
||||||
id: user.id,
|
|
||||||
name: user.displayName,
|
|
||||||
email: user.email,
|
|
||||||
role: user.role,
|
|
||||||
status: MemberStatus.Active,
|
|
||||||
joinedOn: user.createdAt ? String(user.createdAt) : null,
|
|
||||||
updatedAt: user?.updatedAt ? String(user.updatedAt) : null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const pendingInvites: MemberRow[] = (invitesData?.data ?? []).map(
|
|
||||||
(invite) => ({
|
|
||||||
id: `${INVITE_PREFIX}${invite.id}`,
|
|
||||||
name: invite.name ?? '',
|
|
||||||
email: invite.email,
|
|
||||||
role: invite.role,
|
|
||||||
status: MemberStatus.Invited,
|
|
||||||
joinedOn: invite.createdAt ? String(invite.createdAt) : null,
|
|
||||||
token: invite.token ?? null,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return [...activeMembers, ...pendingInvites];
|
|
||||||
}, [usersData, invitesData]);
|
|
||||||
|
|
||||||
const filteredMembers = useMemo((): MemberRow[] => {
|
const filteredMembers = useMemo((): MemberRow[] => {
|
||||||
let result = allMembers;
|
let result = allMembers;
|
||||||
@@ -100,11 +72,6 @@ function MembersSettings(): JSX.Element {
|
|||||||
return result;
|
return result;
|
||||||
}, [allMembers, filterMode, searchQuery]);
|
}, [allMembers, filterMode, searchQuery]);
|
||||||
|
|
||||||
const paginatedMembers = useMemo((): MemberRow[] => {
|
|
||||||
const start = (currentPage - 1) * PAGE_SIZE;
|
|
||||||
return filteredMembers.slice(start, start + PAGE_SIZE);
|
|
||||||
}, [filteredMembers, currentPage]);
|
|
||||||
|
|
||||||
// TODO(nuqs): Replace with nuqs once the nuqs setup and integration is done
|
// TODO(nuqs): Replace with nuqs once the nuqs setup and integration is done
|
||||||
const setPage = useCallback(
|
const setPage = useCallback(
|
||||||
(page: number): void => {
|
(page: number): void => {
|
||||||
@@ -124,7 +91,9 @@ function MembersSettings(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [filteredMembers.length, currentPage, setPage]);
|
}, [filteredMembers.length, currentPage, setPage]);
|
||||||
|
|
||||||
const pendingCount = invitesData?.data?.length ?? 0;
|
const pendingCount = allMembers.filter(
|
||||||
|
(m) => m.status === MemberStatus.Invited,
|
||||||
|
).length;
|
||||||
const totalCount = allMembers.length;
|
const totalCount = allMembers.length;
|
||||||
|
|
||||||
const filterMenuItems: MenuProps['items'] = [
|
const filterMenuItems: MenuProps['items'] = [
|
||||||
@@ -163,8 +132,7 @@ function MembersSettings(): JSX.Element {
|
|||||||
|
|
||||||
const handleInviteComplete = useCallback((): void => {
|
const handleInviteComplete = useCallback((): void => {
|
||||||
refetchUsers();
|
refetchUsers();
|
||||||
refetchInvites();
|
}, [refetchUsers]);
|
||||||
}, [refetchUsers, refetchInvites]);
|
|
||||||
|
|
||||||
const handleRowClick = useCallback((member: MemberRow): void => {
|
const handleRowClick = useCallback((member: MemberRow): void => {
|
||||||
setSelectedMember(member);
|
setSelectedMember(member);
|
||||||
@@ -176,9 +144,8 @@ function MembersSettings(): JSX.Element {
|
|||||||
|
|
||||||
const handleMemberEditComplete = useCallback((): void => {
|
const handleMemberEditComplete = useCallback((): void => {
|
||||||
refetchUsers();
|
refetchUsers();
|
||||||
refetchInvites();
|
|
||||||
setSelectedMember(null);
|
setSelectedMember(null);
|
||||||
}, [refetchUsers, refetchInvites]);
|
}, [refetchUsers]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -232,7 +199,7 @@ function MembersSettings(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MembersTable
|
<MembersTable
|
||||||
data={paginatedMembers}
|
data={filteredMembers}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
total={filteredMembers.length}
|
total={filteredMembers.length}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
@@ -253,7 +220,6 @@ function MembersSettings(): JSX.Element {
|
|||||||
open={selectedMember !== null}
|
open={selectedMember !== null}
|
||||||
onClose={handleDrawerClose}
|
onClose={handleDrawerClose}
|
||||||
onComplete={handleMemberEditComplete}
|
onComplete={handleMemberEditComplete}
|
||||||
onRefetch={handleInviteComplete}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { rest, server } from 'mocks-server/server';
|
import { rest, server } from 'mocks-server/server';
|
||||||
import { render, screen, userEvent } from 'tests/test-utils';
|
import { render, screen, userEvent } from 'tests/test-utils';
|
||||||
import { PendingInvite } from 'types/api/user/getPendingInvites';
|
|
||||||
import { UserResponse } from 'types/api/user/getUser';
|
import { UserResponse } from 'types/api/user/getUser';
|
||||||
|
|
||||||
import MembersSettings from '../MembersSettings';
|
import MembersSettings from '../MembersSettings';
|
||||||
@@ -13,7 +12,6 @@ jest.mock('@signozhq/sonner', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const USERS_ENDPOINT = '*/api/v1/user';
|
const USERS_ENDPOINT = '*/api/v1/user';
|
||||||
const INVITES_ENDPOINT = '*/api/v1/invite';
|
|
||||||
|
|
||||||
const mockUsers: UserResponse[] = [
|
const mockUsers: UserResponse[] = [
|
||||||
{
|
{
|
||||||
@@ -21,7 +19,8 @@ const mockUsers: UserResponse[] = [
|
|||||||
displayName: 'Alice Smith',
|
displayName: 'Alice Smith',
|
||||||
email: 'alice@signoz.io',
|
email: 'alice@signoz.io',
|
||||||
role: 'ADMIN',
|
role: 'ADMIN',
|
||||||
createdAt: 1700000000,
|
status: 'active',
|
||||||
|
createdAt: '2024-01-01T00:00:00.000Z',
|
||||||
organization: 'TestOrg',
|
organization: 'TestOrg',
|
||||||
orgId: 'org-1',
|
orgId: 'org-1',
|
||||||
},
|
},
|
||||||
@@ -30,20 +29,30 @@ const mockUsers: UserResponse[] = [
|
|||||||
displayName: 'Bob Jones',
|
displayName: 'Bob Jones',
|
||||||
email: 'bob@signoz.io',
|
email: 'bob@signoz.io',
|
||||||
role: 'VIEWER',
|
role: 'VIEWER',
|
||||||
createdAt: 1700000001,
|
status: 'active',
|
||||||
|
createdAt: '2024-01-02T00:00:00.000Z',
|
||||||
organization: 'TestOrg',
|
organization: 'TestOrg',
|
||||||
orgId: 'org-1',
|
orgId: 'org-1',
|
||||||
},
|
},
|
||||||
];
|
|
||||||
|
|
||||||
const mockInvites: PendingInvite[] = [
|
|
||||||
{
|
{
|
||||||
id: 'inv-1',
|
id: 'inv-1',
|
||||||
|
displayName: '',
|
||||||
email: 'charlie@signoz.io',
|
email: 'charlie@signoz.io',
|
||||||
name: 'Charlie',
|
|
||||||
role: 'EDITOR',
|
role: 'EDITOR',
|
||||||
createdAt: 1700000002,
|
status: 'pending_invite',
|
||||||
token: 'tok-abc',
|
createdAt: '2024-01-03T00:00:00.000Z',
|
||||||
|
organization: 'TestOrg',
|
||||||
|
orgId: 'org-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'user-3',
|
||||||
|
displayName: 'Dave Deleted',
|
||||||
|
email: 'dave@signoz.io',
|
||||||
|
role: 'VIEWER',
|
||||||
|
status: 'deleted',
|
||||||
|
createdAt: '2024-01-04T00:00:00.000Z',
|
||||||
|
organization: 'TestOrg',
|
||||||
|
orgId: 'org-1',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -54,9 +63,6 @@ describe('MembersSettings (integration)', () => {
|
|||||||
rest.get(USERS_ENDPOINT, (_, res, ctx) =>
|
rest.get(USERS_ENDPOINT, (_, res, ctx) =>
|
||||||
res(ctx.status(200), ctx.json({ data: mockUsers })),
|
res(ctx.status(200), ctx.json({ data: mockUsers })),
|
||||||
),
|
),
|
||||||
rest.get(INVITES_ENDPOINT, (_, res, ctx) =>
|
|
||||||
res(ctx.status(200), ctx.json({ data: mockInvites })),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,14 +70,16 @@ describe('MembersSettings (integration)', () => {
|
|||||||
server.resetHandlers();
|
server.resetHandlers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads and displays active users and pending invites', async () => {
|
it('loads and displays active users, pending invites, and deleted members', async () => {
|
||||||
render(<MembersSettings />);
|
render(<MembersSettings />);
|
||||||
|
|
||||||
await screen.findByText('Alice Smith');
|
await screen.findByText('Alice Smith');
|
||||||
expect(screen.getByText('Bob Jones')).toBeInTheDocument();
|
expect(screen.getByText('Bob Jones')).toBeInTheDocument();
|
||||||
expect(screen.getByText('charlie@signoz.io')).toBeInTheDocument();
|
expect(screen.getByText('charlie@signoz.io')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Dave Deleted')).toBeInTheDocument();
|
||||||
expect(screen.getAllByText('ACTIVE')).toHaveLength(2);
|
expect(screen.getAllByText('ACTIVE')).toHaveLength(2);
|
||||||
expect(screen.getByText('INVITED')).toBeInTheDocument();
|
expect(screen.getByText('INVITED')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('DELETED')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('filters to pending invites via the filter dropdown', async () => {
|
it('filters to pending invites via the filter dropdown', async () => {
|
||||||
@@ -107,7 +115,7 @@ describe('MembersSettings (integration)', () => {
|
|||||||
expect(screen.queryByText('charlie@signoz.io')).not.toBeInTheDocument();
|
expect(screen.queryByText('charlie@signoz.io')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens EditMemberDrawer when a member row is clicked', async () => {
|
it('opens EditMemberDrawer when an active member row is clicked', async () => {
|
||||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
render(<MembersSettings />);
|
render(<MembersSettings />);
|
||||||
@@ -117,6 +125,16 @@ describe('MembersSettings (integration)', () => {
|
|||||||
await screen.findByText('Member Details');
|
await screen.findByText('Member Details');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not open EditMemberDrawer when a deleted member row is clicked', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
render(<MembersSettings />);
|
||||||
|
|
||||||
|
await user.click(await screen.findByText('Dave Deleted'));
|
||||||
|
|
||||||
|
expect(screen.queryByText('Member Details')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('opens InviteMembersModal when "Invite member" button is clicked', async () => {
|
it('opens InviteMembersModal when "Invite member" button is clicked', async () => {
|
||||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
export const INVITE_PREFIX = 'invite-';
|
|
||||||
|
|
||||||
export enum FilterMode {
|
export enum FilterMode {
|
||||||
All = 'all',
|
All = 'all',
|
||||||
Invited = 'invited',
|
Invited = 'invited',
|
||||||
@@ -8,4 +6,25 @@ export enum FilterMode {
|
|||||||
export enum MemberStatus {
|
export enum MemberStatus {
|
||||||
Active = 'Active',
|
Active = 'Active',
|
||||||
Invited = 'Invited',
|
Invited = 'Invited',
|
||||||
|
Deleted = 'Deleted',
|
||||||
|
Anonymous = 'Anonymous',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UserApiStatus {
|
||||||
|
Active = 'active',
|
||||||
|
PendingInvite = 'pending_invite',
|
||||||
|
Deleted = 'deleted',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toMemberStatus(apiStatus: string): MemberStatus {
|
||||||
|
switch (apiStatus) {
|
||||||
|
case UserApiStatus.PendingInvite:
|
||||||
|
return MemberStatus.Invited;
|
||||||
|
case UserApiStatus.Deleted:
|
||||||
|
return MemberStatus.Deleted;
|
||||||
|
case UserApiStatus.Active:
|
||||||
|
return MemberStatus.Active;
|
||||||
|
default:
|
||||||
|
return MemberStatus.Anonymous;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
export const inviteUser = {
|
|
||||||
status: 'success',
|
|
||||||
data: {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
payload: [
|
|
||||||
{
|
|
||||||
email: 'jane@doe.com',
|
|
||||||
name: 'Jane',
|
|
||||||
token: 'testtoken',
|
|
||||||
createdAt: 1715741587,
|
|
||||||
role: 'VIEWER',
|
|
||||||
organization: 'test',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
email: 'test+in@singoz.io',
|
|
||||||
name: '',
|
|
||||||
token: 'testtoken1',
|
|
||||||
createdAt: 1720095913,
|
|
||||||
role: 'VIEWER',
|
|
||||||
organization: 'test',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
getDashboardById,
|
getDashboardById,
|
||||||
} from './__mockdata__/dashboards';
|
} from './__mockdata__/dashboards';
|
||||||
import { explorerView } from './__mockdata__/explorer_views';
|
import { explorerView } from './__mockdata__/explorer_views';
|
||||||
import { inviteUser } from './__mockdata__/invite_user';
|
|
||||||
import { licensesSuccessResponse } from './__mockdata__/licenses';
|
import { licensesSuccessResponse } from './__mockdata__/licenses';
|
||||||
import { membersResponse } from './__mockdata__/members';
|
import { membersResponse } from './__mockdata__/members';
|
||||||
import { queryRangeSuccessResponse } from './__mockdata__/query_range';
|
import { queryRangeSuccessResponse } from './__mockdata__/query_range';
|
||||||
@@ -175,11 +174,14 @@ export const handlers = [
|
|||||||
res(ctx.status(200), ctx.json(getDashboardById)),
|
res(ctx.status(200), ctx.json(getDashboardById)),
|
||||||
),
|
),
|
||||||
|
|
||||||
rest.get('http://localhost/api/v1/invite', (_, res, ctx) =>
|
|
||||||
res(ctx.status(200), ctx.json(inviteUser)),
|
|
||||||
),
|
|
||||||
rest.post('http://localhost/api/v1/invite', (_, res, ctx) =>
|
rest.post('http://localhost/api/v1/invite', (_, res, ctx) =>
|
||||||
res(ctx.status(200), ctx.json(inviteUser)),
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({
|
||||||
|
status: 'success',
|
||||||
|
data: 'invite sent successfully',
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
rest.put('http://localhost/api/v1/user/:id', (_, res, ctx) =>
|
rest.put('http://localhost/api/v1/user/:id', (_, res, ctx) =>
|
||||||
res(
|
res(
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
import { Button } from '@signozhq/button';
|
import { Button } from '@signozhq/button';
|
||||||
import { Callout } from '@signozhq/callout';
|
import { Callout } from '@signozhq/callout';
|
||||||
import { Input } from '@signozhq/input';
|
import { Input } from '@signozhq/input';
|
||||||
import { Form, Input as AntdInput, Typography } from 'antd';
|
import { Form, Input as AntdInput, Typography } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import accept from 'api/v1/invite/id/accept';
|
|
||||||
import getInviteDetails from 'api/v1/invite/id/get';
|
|
||||||
import signUpApi from 'api/v1/register/post';
|
import signUpApi from 'api/v1/register/post';
|
||||||
import passwordAuthNContext from 'api/v2/sessions/email_password/post';
|
import passwordAuthNContext from 'api/v2/sessions/email_password/post';
|
||||||
import afterLogin from 'AppRoutes/utils';
|
import afterLogin from 'AppRoutes/utils';
|
||||||
@@ -15,9 +11,7 @@ import AuthError from 'components/AuthError/AuthError';
|
|||||||
import AuthPageContainer from 'components/AuthPageContainer';
|
import AuthPageContainer from 'components/AuthPageContainer';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { ArrowRight, CircleAlert } from 'lucide-react';
|
import { ArrowRight, CircleAlert } from 'lucide-react';
|
||||||
import { SuccessResponseV2 } from 'types/api';
|
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
import { InviteDetails } from 'types/api/user/getInviteDetails';
|
|
||||||
|
|
||||||
import { FormContainer, Label } from './styles';
|
import { FormContainer, Label } from './styles';
|
||||||
|
|
||||||
@@ -39,22 +33,6 @@ function SignUp(): JSX.Element {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
const [formError, setFormError] = useState<APIError | null>();
|
const [formError, setFormError] = useState<APIError | null>();
|
||||||
const { search } = useLocation();
|
|
||||||
const params = new URLSearchParams(search);
|
|
||||||
const token = params.get('token');
|
|
||||||
const [isDetailsDisable, setIsDetailsDisable] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const getInviteDetailsResponse = useQuery<
|
|
||||||
SuccessResponseV2<InviteDetails>,
|
|
||||||
APIError
|
|
||||||
>({
|
|
||||||
queryFn: () =>
|
|
||||||
getInviteDetails({
|
|
||||||
inviteId: token || '',
|
|
||||||
}),
|
|
||||||
queryKey: ['getInviteDetails', token],
|
|
||||||
enabled: token !== null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const [form] = Form.useForm<FormValues>();
|
const [form] = Form.useForm<FormValues>();
|
||||||
@@ -64,49 +42,6 @@ function SignUp(): JSX.Element {
|
|||||||
const password = Form.useWatch('password', form);
|
const password = Form.useWatch('password', form);
|
||||||
const confirmPassword = Form.useWatch('confirmPassword', form);
|
const confirmPassword = Form.useWatch('confirmPassword', form);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
getInviteDetailsResponse.status === 'success' &&
|
|
||||||
getInviteDetailsResponse.data.data
|
|
||||||
) {
|
|
||||||
const responseDetails = getInviteDetailsResponse.data.data;
|
|
||||||
form.setFieldValue('email', responseDetails.email);
|
|
||||||
form.setFieldValue('organizationName', responseDetails.organization);
|
|
||||||
setIsDetailsDisable(true);
|
|
||||||
|
|
||||||
logEvent('Account Creation Page Visited', {
|
|
||||||
email: responseDetails.email,
|
|
||||||
name: responseDetails.name,
|
|
||||||
company_name: responseDetails.organization,
|
|
||||||
source: 'SigNoz Cloud',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
getInviteDetailsResponse.data?.data,
|
|
||||||
form,
|
|
||||||
getInviteDetailsResponse.status,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
getInviteDetailsResponse.status === 'success' &&
|
|
||||||
getInviteDetailsResponse?.error
|
|
||||||
) {
|
|
||||||
const { error } = getInviteDetailsResponse;
|
|
||||||
notifications.error({
|
|
||||||
message: (error as APIError).getErrorCode(),
|
|
||||||
description: (error as APIError).getErrorMessage(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
getInviteDetailsResponse,
|
|
||||||
getInviteDetailsResponse.data,
|
|
||||||
getInviteDetailsResponse.status,
|
|
||||||
notifications,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const isSignUp = token === null;
|
|
||||||
|
|
||||||
const signUp = async (values: FormValues): Promise<void> => {
|
const signUp = async (values: FormValues): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { organizationName, password, email } = values;
|
const { organizationName, password, email } = values;
|
||||||
@@ -114,7 +49,6 @@ function SignUp(): JSX.Element {
|
|||||||
email,
|
email,
|
||||||
orgDisplayName: organizationName,
|
orgDisplayName: organizationName,
|
||||||
password,
|
password,
|
||||||
token: params.get('token') || undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const token = await passwordAuthNContext({
|
const token = await passwordAuthNContext({
|
||||||
@@ -129,25 +63,6 @@ function SignUp(): JSX.Element {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const acceptInvite = async (values: FormValues): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { password, email } = values;
|
|
||||||
const user = await accept({
|
|
||||||
password,
|
|
||||||
token: params.get('token') || '',
|
|
||||||
});
|
|
||||||
const token = await passwordAuthNContext({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
orgId: user.data.orgId,
|
|
||||||
});
|
|
||||||
|
|
||||||
await afterLogin(token.data.accessToken, token.data.refreshToken);
|
|
||||||
} catch (error) {
|
|
||||||
setFormError(error as APIError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (): void => {
|
const handleSubmit = (): void => {
|
||||||
(async (): Promise<void> => {
|
(async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -155,14 +70,10 @@ function SignUp(): JSX.Element {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setFormError(null);
|
setFormError(null);
|
||||||
|
|
||||||
if (isSignUp) {
|
await signUp(values);
|
||||||
await signUp(values);
|
logEvent('Account Created Successfully', {
|
||||||
logEvent('Account Created Successfully', {
|
email: values.email,
|
||||||
email: values.email,
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await acceptInvite(values);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -247,7 +158,6 @@ function SignUp(): JSX.Element {
|
|||||||
autoFocus
|
autoFocus
|
||||||
required
|
required
|
||||||
id="signupEmail"
|
id="signupEmail"
|
||||||
disabled={isDetailsDisable}
|
|
||||||
className="signup-form-input"
|
className="signup-form-input"
|
||||||
/>
|
/>
|
||||||
</FormContainer.Item>
|
</FormContainer.Item>
|
||||||
@@ -291,15 +201,13 @@ function SignUp(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isSignUp && (
|
<Callout
|
||||||
<Callout
|
type="info"
|
||||||
type="info"
|
size="small"
|
||||||
size="small"
|
showIcon
|
||||||
showIcon
|
className="signup-info-callout"
|
||||||
className="signup-info-callout"
|
description="This will create an admin account. If you are not an admin, please ask your admin for an invite link"
|
||||||
description="This will create an admin account. If you are not an admin, please ask your admin for an invite link"
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{confirmPasswordError && (
|
{confirmPasswordError && (
|
||||||
<Callout
|
<Callout
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import afterLogin from 'AppRoutes/utils';
|
import afterLogin from 'AppRoutes/utils';
|
||||||
import { rest, server } from 'mocks-server/server';
|
import { rest, server } from 'mocks-server/server';
|
||||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||||
import { InviteDetails } from 'types/api/user/getInviteDetails';
|
|
||||||
import { SignupResponse } from 'types/api/v1/register/post';
|
import { SignupResponse } from 'types/api/v1/register/post';
|
||||||
import { Token } from 'types/api/v2/sessions/email_password/post';
|
import { Token } from 'types/api/v2/sessions/email_password/post';
|
||||||
|
|
||||||
@@ -32,14 +31,8 @@ jest.mock('lib/history', () => ({
|
|||||||
|
|
||||||
const REGISTER_ENDPOINT = '*/api/v1/register';
|
const REGISTER_ENDPOINT = '*/api/v1/register';
|
||||||
const EMAIL_PASSWORD_ENDPOINT = '*/api/v2/sessions/email_password';
|
const EMAIL_PASSWORD_ENDPOINT = '*/api/v2/sessions/email_password';
|
||||||
const INVITE_DETAILS_ENDPOINT = '*/api/v1/invite/*';
|
|
||||||
const ACCEPT_INVITE_ENDPOINT = '*/api/v1/invite/accept';
|
|
||||||
|
|
||||||
interface MockSignupResponse extends SignupResponse {
|
const mockSignupResponse: SignupResponse = {
|
||||||
orgId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockSignupResponse: MockSignupResponse = {
|
|
||||||
orgId: 'test-org-id',
|
orgId: 'test-org-id',
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
email: 'test@signoz.io',
|
email: 'test@signoz.io',
|
||||||
@@ -53,15 +46,6 @@ const mockTokenResponse: Token = {
|
|||||||
refreshToken: 'mock-refresh-token',
|
refreshToken: 'mock-refresh-token',
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockInviteDetails: InviteDetails = {
|
|
||||||
email: 'invited@signoz.io',
|
|
||||||
name: 'Invited User',
|
|
||||||
organization: 'Test Org',
|
|
||||||
createdAt: Date.now(),
|
|
||||||
role: 'ADMIN',
|
|
||||||
token: 'invite-token-123',
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('SignUp Component - Regular Signup', () => {
|
describe('SignUp Component - Regular Signup', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
@@ -288,242 +272,3 @@ describe('SignUp Component - Regular Signup', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('SignUp Component - Accept Invite', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
window.history.pushState({}, '', '/signup?token=invite-token-123');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
server.resetHandlers();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Initial Render with Invite', () => {
|
|
||||||
it('pre-fills form fields from invite details', async () => {
|
|
||||||
server.use(
|
|
||||||
rest.get(INVITE_DETAILS_ENDPOINT, (_req, res, ctx) =>
|
|
||||||
res(
|
|
||||||
ctx.status(200),
|
|
||||||
ctx.json({
|
|
||||||
data: mockInviteDetails,
|
|
||||||
status: 'success',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
render(<SignUp />, undefined, {
|
|
||||||
initialRoute: '/signup?token=invite-token-123',
|
|
||||||
});
|
|
||||||
|
|
||||||
const emailInput = await screen.findByLabelText(/email address/i);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(emailInput).toHaveValue('invited@signoz.io');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('disables email field when invite details are loaded', async () => {
|
|
||||||
server.use(
|
|
||||||
rest.get(INVITE_DETAILS_ENDPOINT, (_req, res, ctx) =>
|
|
||||||
res(
|
|
||||||
ctx.status(200),
|
|
||||||
ctx.json({
|
|
||||||
data: mockInviteDetails,
|
|
||||||
status: 'success',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
render(<SignUp />, undefined, {
|
|
||||||
initialRoute: '/signup?token=invite-token-123',
|
|
||||||
});
|
|
||||||
|
|
||||||
const emailInput = await screen.findByLabelText(/email address/i);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(emailInput).toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not show admin account info callout for invite flow', async () => {
|
|
||||||
server.use(
|
|
||||||
rest.get(INVITE_DETAILS_ENDPOINT, (_req, res, ctx) =>
|
|
||||||
res(
|
|
||||||
ctx.status(200),
|
|
||||||
ctx.json({
|
|
||||||
data: mockInviteDetails,
|
|
||||||
status: 'success',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
render(<SignUp />, undefined, {
|
|
||||||
initialRoute: '/signup?token=invite-token-123',
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(
|
|
||||||
screen.queryByText(/this will create an admin account/i),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Successful Invite Acceptance', () => {
|
|
||||||
it('successfully accepts invite and logs in user', async () => {
|
|
||||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
|
||||||
|
|
||||||
server.use(
|
|
||||||
rest.get(INVITE_DETAILS_ENDPOINT, (_req, res, ctx) =>
|
|
||||||
res(
|
|
||||||
ctx.status(200),
|
|
||||||
ctx.json({
|
|
||||||
data: mockInviteDetails,
|
|
||||||
status: 'success',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
rest.post(ACCEPT_INVITE_ENDPOINT, (_req, res, ctx) =>
|
|
||||||
res(
|
|
||||||
ctx.status(200),
|
|
||||||
ctx.json({
|
|
||||||
data: mockSignupResponse,
|
|
||||||
status: 'success',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
rest.post(EMAIL_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
|
||||||
res(
|
|
||||||
ctx.status(200),
|
|
||||||
ctx.json({
|
|
||||||
data: mockTokenResponse,
|
|
||||||
status: 'success',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
render(<SignUp />, undefined, {
|
|
||||||
initialRoute: '/signup?token=invite-token-123',
|
|
||||||
});
|
|
||||||
|
|
||||||
const emailInput = await screen.findByLabelText(/email address/i);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(emailInput).toHaveValue('invited@signoz.io');
|
|
||||||
});
|
|
||||||
|
|
||||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
|
||||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
|
||||||
/confirm your new password/i,
|
|
||||||
);
|
|
||||||
const submitButton = screen.getByRole('button', {
|
|
||||||
name: /access my workspace/i,
|
|
||||||
});
|
|
||||||
|
|
||||||
await user.type(passwordInput, 'password123');
|
|
||||||
await user.type(confirmPasswordInput, 'password123');
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(submitButton).not.toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
await user.click(submitButton);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockAfterLogin).toHaveBeenCalledWith(
|
|
||||||
'mock-access-token',
|
|
||||||
'mock-refresh-token',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Error Handling for Invite', () => {
|
|
||||||
it('displays error when invite details fetch fails', async () => {
|
|
||||||
server.use(
|
|
||||||
rest.get(INVITE_DETAILS_ENDPOINT, (_req, res, ctx) =>
|
|
||||||
res(
|
|
||||||
ctx.status(404),
|
|
||||||
ctx.json({
|
|
||||||
error: {
|
|
||||||
code: 'INVITE_NOT_FOUND',
|
|
||||||
message: 'Invite not found',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
render(<SignUp />, undefined, {
|
|
||||||
initialRoute: '/signup?token=invalid-token',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify form is still accessible and fields are enabled
|
|
||||||
const emailInput = await screen.findByLabelText(/email address/i);
|
|
||||||
|
|
||||||
expect(emailInput).toBeInTheDocument();
|
|
||||||
expect(emailInput).not.toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays error when accept invite API fails', async () => {
|
|
||||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
|
||||||
|
|
||||||
server.use(
|
|
||||||
rest.get(INVITE_DETAILS_ENDPOINT, (_req, res, ctx) =>
|
|
||||||
res(
|
|
||||||
ctx.status(200),
|
|
||||||
ctx.json({
|
|
||||||
data: mockInviteDetails,
|
|
||||||
status: 'success',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
rest.post(ACCEPT_INVITE_ENDPOINT, (_req, res, ctx) =>
|
|
||||||
res(
|
|
||||||
ctx.status(400),
|
|
||||||
ctx.json({
|
|
||||||
error: {
|
|
||||||
code: 'INVALID_TOKEN',
|
|
||||||
message: 'Invalid or expired invite token',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
render(<SignUp />, undefined, {
|
|
||||||
initialRoute: '/signup?token=expired-token',
|
|
||||||
});
|
|
||||||
|
|
||||||
const emailInput = await screen.findByLabelText(/email address/i);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(emailInput).toHaveValue('invited@signoz.io');
|
|
||||||
});
|
|
||||||
|
|
||||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
|
||||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
|
||||||
/confirm your new password/i,
|
|
||||||
);
|
|
||||||
const submitButton = screen.getByRole('button', {
|
|
||||||
name: /access my workspace/i,
|
|
||||||
});
|
|
||||||
|
|
||||||
await user.type(passwordInput, 'password123');
|
|
||||||
await user.type(confirmPasswordInput, 'password123');
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(submitButton).not.toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
await user.click(submitButton);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await screen.findByText(/invalid or expired invite token/i),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { UserResponse } from './getUser';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
token: string;
|
|
||||||
password: string;
|
|
||||||
displayName?: string;
|
|
||||||
sourceUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LoginPrecheckResponse {
|
|
||||||
sso: boolean;
|
|
||||||
ssoUrl?: string;
|
|
||||||
canSelfRegister?: boolean;
|
|
||||||
isUser: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PayloadProps {
|
|
||||||
data: UserResponse;
|
|
||||||
status: string;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface Props {
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PayloadProps {
|
|
||||||
data: string;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { User } from 'types/reducer/app';
|
|
||||||
import { ROLES } from 'types/roles';
|
|
||||||
|
|
||||||
import { Organization } from './getOrganization';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
inviteId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PayloadProps {
|
|
||||||
data: InviteDetails;
|
|
||||||
status: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InviteDetails {
|
|
||||||
createdAt: number;
|
|
||||||
email: User['email'];
|
|
||||||
name: User['displayName'];
|
|
||||||
role: ROLES;
|
|
||||||
token: string;
|
|
||||||
organization: Organization['displayName'];
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { User } from 'types/reducer/app';
|
|
||||||
import { ROLES } from 'types/roles';
|
|
||||||
|
|
||||||
export interface PendingInvite {
|
|
||||||
createdAt: number;
|
|
||||||
email: User['email'];
|
|
||||||
name: User['displayName'];
|
|
||||||
role: ROLES;
|
|
||||||
id: string;
|
|
||||||
token: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PayloadProps = {
|
|
||||||
data: PendingInvite[];
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
@@ -7,14 +7,16 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UserResponse {
|
export interface UserResponse {
|
||||||
createdAt: number;
|
createdAt: number | string;
|
||||||
email: string;
|
email: string;
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
role: ROLES;
|
role: ROLES;
|
||||||
updatedAt?: number;
|
updatedAt?: number | string;
|
||||||
|
isRoot?: boolean;
|
||||||
|
status?: 'active' | 'pending_invite' | 'deleted';
|
||||||
}
|
}
|
||||||
export interface PayloadProps {
|
export interface PayloadProps {
|
||||||
data: UserResponse;
|
data: UserResponse;
|
||||||
|
|||||||
@@ -4506,6 +4506,19 @@
|
|||||||
"@radix-ui/react-use-callback-ref" "1.1.1"
|
"@radix-ui/react-use-callback-ref" "1.1.1"
|
||||||
"@radix-ui/react-use-escape-keydown" "1.1.1"
|
"@radix-ui/react-use-escape-keydown" "1.1.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-dropdown-menu@^2.1.16":
|
||||||
|
version "2.1.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz#5ee045c62bad8122347981c479d92b1ff24c7254"
|
||||||
|
integrity sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/primitive" "1.1.3"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.2"
|
||||||
|
"@radix-ui/react-context" "1.1.2"
|
||||||
|
"@radix-ui/react-id" "1.1.1"
|
||||||
|
"@radix-ui/react-menu" "2.1.16"
|
||||||
|
"@radix-ui/react-primitive" "2.1.3"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.2.2"
|
||||||
|
|
||||||
"@radix-ui/react-focus-guards@1.0.0":
|
"@radix-ui/react-focus-guards@1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz#339c1c69c41628c1a5e655f15f7020bf11aa01fa"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz#339c1c69c41628c1a5e655f15f7020bf11aa01fa"
|
||||||
@@ -4565,6 +4578,30 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-use-layout-effect" "1.1.1"
|
"@radix-ui/react-use-layout-effect" "1.1.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-menu@2.1.16":
|
||||||
|
version "2.1.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.16.tgz#528a5a973c3a7413d3d49eb9ccd229aa52402911"
|
||||||
|
integrity sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/primitive" "1.1.3"
|
||||||
|
"@radix-ui/react-collection" "1.1.7"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.2"
|
||||||
|
"@radix-ui/react-context" "1.1.2"
|
||||||
|
"@radix-ui/react-direction" "1.1.1"
|
||||||
|
"@radix-ui/react-dismissable-layer" "1.1.11"
|
||||||
|
"@radix-ui/react-focus-guards" "1.1.3"
|
||||||
|
"@radix-ui/react-focus-scope" "1.1.7"
|
||||||
|
"@radix-ui/react-id" "1.1.1"
|
||||||
|
"@radix-ui/react-popper" "1.2.8"
|
||||||
|
"@radix-ui/react-portal" "1.1.9"
|
||||||
|
"@radix-ui/react-presence" "1.1.5"
|
||||||
|
"@radix-ui/react-primitive" "2.1.3"
|
||||||
|
"@radix-ui/react-roving-focus" "1.1.11"
|
||||||
|
"@radix-ui/react-slot" "1.2.3"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.1"
|
||||||
|
aria-hidden "^1.2.4"
|
||||||
|
react-remove-scroll "^2.6.3"
|
||||||
|
|
||||||
"@radix-ui/react-popover@^1.1.15", "@radix-ui/react-popover@^1.1.2":
|
"@radix-ui/react-popover@^1.1.15", "@radix-ui/react-popover@^1.1.2":
|
||||||
version "1.1.15"
|
version "1.1.15"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.15.tgz#9c852f93990a687ebdc949b2c3de1f37cdc4c5d5"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.15.tgz#9c852f93990a687ebdc949b2c3de1f37cdc4c5d5"
|
||||||
@@ -4804,6 +4841,20 @@
|
|||||||
"@radix-ui/react-roving-focus" "1.0.4"
|
"@radix-ui/react-roving-focus" "1.0.4"
|
||||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-tabs@^1.1.3":
|
||||||
|
version "1.1.13"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz#3537ce379d7e7ff4eeb6b67a0973e139c2ac1f15"
|
||||||
|
integrity sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/primitive" "1.1.3"
|
||||||
|
"@radix-ui/react-context" "1.1.2"
|
||||||
|
"@radix-ui/react-direction" "1.1.1"
|
||||||
|
"@radix-ui/react-id" "1.1.1"
|
||||||
|
"@radix-ui/react-presence" "1.1.5"
|
||||||
|
"@radix-ui/react-primitive" "2.1.3"
|
||||||
|
"@radix-ui/react-roving-focus" "1.1.11"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.2.2"
|
||||||
|
|
||||||
"@radix-ui/react-toggle-group@^1.1.7":
|
"@radix-ui/react-toggle-group@^1.1.7":
|
||||||
version "1.1.11"
|
version "1.1.11"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz#e513d6ffdb07509b400ab5b26f2523747c0d51c1"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz#e513d6ffdb07509b400ab5b26f2523747c0d51c1"
|
||||||
@@ -5675,6 +5726,42 @@
|
|||||||
tailwind-merge "^2.5.2"
|
tailwind-merge "^2.5.2"
|
||||||
tailwindcss-animate "^1.0.7"
|
tailwindcss-animate "^1.0.7"
|
||||||
|
|
||||||
|
"@signozhq/ui@0.0.5":
|
||||||
|
version "0.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@signozhq/ui/-/ui-0.0.5.tgz#8badef53416b7ace0fe61ff01ff3da679a0e4ba5"
|
||||||
|
integrity sha512-4vPvUh3rwpst068qXUZ26JfCQGv1vo1xMSwtKw6wTjiiq1Bf3geP84HWVXycNMIrIeVnUgDGnqe0D4doh+mL8A==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-checkbox" "^1.2.3"
|
||||||
|
"@radix-ui/react-dialog" "^1.1.11"
|
||||||
|
"@radix-ui/react-dropdown-menu" "^2.1.16"
|
||||||
|
"@radix-ui/react-icons" "^1.3.0"
|
||||||
|
"@radix-ui/react-popover" "^1.1.15"
|
||||||
|
"@radix-ui/react-radio-group" "^1.3.4"
|
||||||
|
"@radix-ui/react-slot" "^1.2.3"
|
||||||
|
"@radix-ui/react-switch" "^1.1.4"
|
||||||
|
"@radix-ui/react-tabs" "^1.1.3"
|
||||||
|
"@radix-ui/react-toggle" "^1.1.6"
|
||||||
|
"@radix-ui/react-toggle-group" "^1.1.7"
|
||||||
|
"@radix-ui/react-tooltip" "^1.2.6"
|
||||||
|
"@tanstack/react-table" "^8.21.3"
|
||||||
|
"@tanstack/react-virtual" "^3.13.9"
|
||||||
|
"@types/lodash-es" "^4.17.12"
|
||||||
|
class-variance-authority "^0.7.0"
|
||||||
|
clsx "^2.1.1"
|
||||||
|
cmdk "^1.1.1"
|
||||||
|
date-fns "^4.1.0"
|
||||||
|
dayjs "^1.11.10"
|
||||||
|
lodash-es "^4.17.21"
|
||||||
|
lucide-react "^0.445.0"
|
||||||
|
lucide-solid "^0.510.0"
|
||||||
|
motion "^11.11.17"
|
||||||
|
next-themes "^0.4.6"
|
||||||
|
nuqs "^2.8.9"
|
||||||
|
react-day-picker "^9.8.1"
|
||||||
|
react-resizable-panels "^4.7.1"
|
||||||
|
sonner "^2.0.7"
|
||||||
|
tailwind-merge "^3.5.0"
|
||||||
|
|
||||||
"@sinclair/typebox@^0.25.16":
|
"@sinclair/typebox@^0.25.16":
|
||||||
version "0.25.24"
|
version "0.25.24"
|
||||||
resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz"
|
resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz"
|
||||||
@@ -9573,6 +9660,11 @@ dayjs@^1.10.7, dayjs@^1.11.1:
|
|||||||
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz"
|
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz"
|
||||||
integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
|
integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
|
||||||
|
|
||||||
|
dayjs@^1.11.10:
|
||||||
|
version "1.11.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.20.tgz#88d919fd639dc991415da5f4cb6f1b6650811938"
|
||||||
|
integrity sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==
|
||||||
|
|
||||||
debounce@^1.2.1:
|
debounce@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
|
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
|
||||||
@@ -11092,6 +11184,15 @@ fraction.js@^4.3.7:
|
|||||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
||||||
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
||||||
|
|
||||||
|
framer-motion@^11.18.2:
|
||||||
|
version "11.18.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.18.2.tgz#0c6bd05677f4cfd3b3bdead4eb5ecdd5ed245718"
|
||||||
|
integrity sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==
|
||||||
|
dependencies:
|
||||||
|
motion-dom "^11.18.1"
|
||||||
|
motion-utils "^11.18.1"
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
framer-motion@^12.4.13:
|
framer-motion@^12.4.13:
|
||||||
version "12.4.13"
|
version "12.4.13"
|
||||||
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.4.13.tgz#1efd954f95e6a54685b660929c00f5a61e35256a"
|
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.4.13.tgz#1efd954f95e6a54685b660929c00f5a61e35256a"
|
||||||
@@ -15002,6 +15103,13 @@ moment@^2.29.4:
|
|||||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
||||||
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||||
|
|
||||||
|
motion-dom@^11.18.1:
|
||||||
|
version "11.18.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-11.18.1.tgz#e7fed7b7dc6ae1223ef1cce29ee54bec826dc3f2"
|
||||||
|
integrity sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==
|
||||||
|
dependencies:
|
||||||
|
motion-utils "^11.18.1"
|
||||||
|
|
||||||
motion-dom@^12.4.11:
|
motion-dom@^12.4.11:
|
||||||
version "12.4.11"
|
version "12.4.11"
|
||||||
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.4.11.tgz#0419c8686cda4d523f08249deeb8fa6683a9b9d3"
|
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.4.11.tgz#0419c8686cda4d523f08249deeb8fa6683a9b9d3"
|
||||||
@@ -15009,6 +15117,11 @@ motion-dom@^12.4.11:
|
|||||||
dependencies:
|
dependencies:
|
||||||
motion-utils "^12.4.10"
|
motion-utils "^12.4.10"
|
||||||
|
|
||||||
|
motion-utils@^11.18.1:
|
||||||
|
version "11.18.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-11.18.1.tgz#671227669833e991c55813cf337899f41327db5b"
|
||||||
|
integrity sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==
|
||||||
|
|
||||||
motion-utils@^12.4.10:
|
motion-utils@^12.4.10:
|
||||||
version "12.4.10"
|
version "12.4.10"
|
||||||
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.4.10.tgz#3d93acea5454419eaaad8d5e5425cb71cbfa1e7f"
|
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.4.10.tgz#3d93acea5454419eaaad8d5e5425cb71cbfa1e7f"
|
||||||
@@ -15022,6 +15135,14 @@ motion@12.4.13:
|
|||||||
framer-motion "^12.4.13"
|
framer-motion "^12.4.13"
|
||||||
tslib "^2.4.0"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
|
motion@^11.11.17:
|
||||||
|
version "11.18.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/motion/-/motion-11.18.2.tgz#17fb372f3ed94fc9ee1384a25a9068e9da1951e7"
|
||||||
|
integrity sha512-JLjvFDuFr42NFtcVoMAyC2sEjnpA8xpy6qWPyzQvCloznAyQ8FIXioxWfHiLtgYhoVpfUqSWpn1h9++skj9+Wg==
|
||||||
|
dependencies:
|
||||||
|
framer-motion "^11.18.2"
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
mri@^1.1.0:
|
mri@^1.1.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
||||||
@@ -15292,6 +15413,13 @@ nuqs@2.8.8:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@standard-schema/spec" "1.0.0"
|
"@standard-schema/spec" "1.0.0"
|
||||||
|
|
||||||
|
nuqs@^2.8.9:
|
||||||
|
version "2.8.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/nuqs/-/nuqs-2.8.9.tgz#e2c27d87c0dd0e3b4412fe867bcd0947cc4c998f"
|
||||||
|
integrity sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ==
|
||||||
|
dependencies:
|
||||||
|
"@standard-schema/spec" "1.0.0"
|
||||||
|
|
||||||
nwsapi@^2.2.2:
|
nwsapi@^2.2.2:
|
||||||
version "2.2.23"
|
version "2.2.23"
|
||||||
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.23.tgz#59712c3a88e6de2bb0b6ccc1070397267019cf6c"
|
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.23.tgz#59712c3a88e6de2bb0b6ccc1070397267019cf6c"
|
||||||
@@ -16957,6 +17085,11 @@ react-resizable-panels@^3.0.5:
|
|||||||
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-3.0.5.tgz#50a20645263eed02344de4a70d1319bbc0014bbd"
|
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-3.0.5.tgz#50a20645263eed02344de4a70d1319bbc0014bbd"
|
||||||
integrity sha512-3z1yN25DMTXLg2wfyFrW32r5k4WEcUa3F7cJ2EgtNK07lnOs4mpM8yWLGunCpkhcQRwJX4fqoLcIh/pHPxzlmQ==
|
integrity sha512-3z1yN25DMTXLg2wfyFrW32r5k4WEcUa3F7cJ2EgtNK07lnOs4mpM8yWLGunCpkhcQRwJX4fqoLcIh/pHPxzlmQ==
|
||||||
|
|
||||||
|
react-resizable-panels@^4.7.1:
|
||||||
|
version "4.7.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-4.7.3.tgz#4040aa0f5c5c4cc4bb685cb69973601ccda3b014"
|
||||||
|
integrity sha512-PYcYMLtvJD+Pr0TQNeMvddcnLOwUa/Yb4iNwU7ThNLlHaQYEEC9MIBWHaBGODzYuXIkPRZ/OWe5sbzG1Rzq5ew==
|
||||||
|
|
||||||
react-resizable@3.0.4:
|
react-resizable@3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.4.tgz"
|
resolved "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.4.tgz"
|
||||||
@@ -18797,6 +18930,11 @@ tailwind-merge@^2.5.2:
|
|||||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz#ac5fb7e227910c038d458f396b7400d93a3142d5"
|
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz#ac5fb7e227910c038d458f396b7400d93a3142d5"
|
||||||
integrity sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==
|
integrity sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==
|
||||||
|
|
||||||
|
tailwind-merge@^3.5.0:
|
||||||
|
version "3.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.5.0.tgz#06502f4496ba15151445d97d916a26564d50d1ca"
|
||||||
|
integrity sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==
|
||||||
|
|
||||||
tailwindcss-animate@^1.0.7:
|
tailwindcss-animate@^1.0.7:
|
||||||
version "1.0.7"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4"
|
resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4"
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -11,7 +11,6 @@ require (
|
|||||||
github.com/SigNoz/signoz-otel-collector v0.144.2
|
github.com/SigNoz/signoz-otel-collector v0.144.2
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||||
github.com/antonmedv/expr v1.15.3
|
github.com/antonmedv/expr v1.15.3
|
||||||
github.com/bytedance/sonic v1.14.1
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0
|
github.com/cespare/xxhash/v2 v2.3.0
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0
|
github.com/coreos/go-oidc/v3 v3.17.0
|
||||||
github.com/dgraph-io/ristretto/v2 v2.3.0
|
github.com/dgraph-io/ristretto/v2 v2.3.0
|
||||||
@@ -106,6 +105,7 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
||||||
github.com/aws/smithy-go v1.24.0 // indirect
|
github.com/aws/smithy-go v1.24.0 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic v1.14.1 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
|
|||||||
@@ -43,74 +43,6 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := router.Handle("/api/v1/invite/{token}", handler.New(provider.authZ.OpenAccess(provider.userHandler.GetInvite), handler.OpenAPIDef{
|
|
||||||
ID: "GetInvite",
|
|
||||||
Tags: []string{"users"},
|
|
||||||
Summary: "Get invite",
|
|
||||||
Description: "This endpoint gets an invite by token",
|
|
||||||
Request: nil,
|
|
||||||
RequestContentType: "",
|
|
||||||
Response: new(types.Invite),
|
|
||||||
ResponseContentType: "application/json",
|
|
||||||
SuccessStatusCode: http.StatusOK,
|
|
||||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
|
||||||
Deprecated: false,
|
|
||||||
SecuritySchemes: []handler.OpenAPISecurityScheme{},
|
|
||||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := router.Handle("/api/v1/invite/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.DeleteInvite), handler.OpenAPIDef{
|
|
||||||
ID: "DeleteInvite",
|
|
||||||
Tags: []string{"users"},
|
|
||||||
Summary: "Delete invite",
|
|
||||||
Description: "This endpoint deletes an invite by id",
|
|
||||||
Request: nil,
|
|
||||||
RequestContentType: "",
|
|
||||||
Response: nil,
|
|
||||||
ResponseContentType: "",
|
|
||||||
SuccessStatusCode: http.StatusNoContent,
|
|
||||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
|
||||||
Deprecated: false,
|
|
||||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
|
||||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := router.Handle("/api/v1/invite", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListInvite), handler.OpenAPIDef{
|
|
||||||
ID: "ListInvite",
|
|
||||||
Tags: []string{"users"},
|
|
||||||
Summary: "List invites",
|
|
||||||
Description: "This endpoint lists all invites",
|
|
||||||
Request: nil,
|
|
||||||
RequestContentType: "",
|
|
||||||
Response: make([]*types.Invite, 0),
|
|
||||||
ResponseContentType: "application/json",
|
|
||||||
SuccessStatusCode: http.StatusOK,
|
|
||||||
ErrorStatusCodes: []int{},
|
|
||||||
Deprecated: false,
|
|
||||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
|
||||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := router.Handle("/api/v1/invite/accept", handler.New(provider.authZ.OpenAccess(provider.userHandler.AcceptInvite), handler.OpenAPIDef{
|
|
||||||
ID: "AcceptInvite",
|
|
||||||
Tags: []string{"users"},
|
|
||||||
Summary: "Accept invite",
|
|
||||||
Description: "This endpoint accepts an invite by token",
|
|
||||||
Request: new(types.PostableAcceptInvite),
|
|
||||||
RequestContentType: "application/json",
|
|
||||||
Response: new(types.User),
|
|
||||||
ResponseContentType: "application/json",
|
|
||||||
SuccessStatusCode: http.StatusCreated,
|
|
||||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
|
||||||
Deprecated: false,
|
|
||||||
SecuritySchemes: []handler.OpenAPISecurityScheme{},
|
|
||||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := router.Handle("/api/v1/pats", handler.New(provider.authZ.AdminAccess(provider.userHandler.CreateAPIKey), handler.OpenAPIDef{
|
if err := router.Handle("/api/v1/pats", handler.New(provider.authZ.AdminAccess(provider.userHandler.CreateAPIKey), handler.OpenAPIDef{
|
||||||
ID: "CreateAPIKey",
|
ID: "CreateAPIKey",
|
||||||
Tags: []string{"users"},
|
Tags: []string{"users"},
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/http/render"
|
"github.com/SigNoz/signoz/pkg/http/render"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
@@ -41,9 +40,7 @@ func (middleware *AuthZ) ViewAccess(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
commentCtx := ctxtypes.CommentFromContext(ctx)
|
if claims.IdentNProvider == authtypes.IdentNProviderAPIkey.StringValue() {
|
||||||
authtype, ok := commentCtx.Map()["auth_type"]
|
|
||||||
if ok && (authtype == authtypes.IdentNProviderAPIkey.StringValue()) {
|
|
||||||
if err := claims.IsViewer(); err != nil {
|
if err := claims.IsViewer(); err != nil {
|
||||||
middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims)
|
middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims)
|
||||||
render.Error(rw, err)
|
render.Error(rw, err)
|
||||||
@@ -93,9 +90,7 @@ func (middleware *AuthZ) EditAccess(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
commentCtx := ctxtypes.CommentFromContext(ctx)
|
if claims.IdentNProvider == authtypes.IdentNProviderAPIkey.StringValue() {
|
||||||
authtype, ok := commentCtx.Map()["auth_type"]
|
|
||||||
if ok && (authtype == authtypes.IdentNProviderAPIkey.StringValue()) {
|
|
||||||
if err := claims.IsEditor(); err != nil {
|
if err := claims.IsEditor(); err != nil {
|
||||||
middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims)
|
middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims)
|
||||||
render.Error(rw, err)
|
render.Error(rw, err)
|
||||||
@@ -144,9 +139,7 @@ func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
commentCtx := ctxtypes.CommentFromContext(ctx)
|
if claims.IdentNProvider == authtypes.IdentNProviderAPIkey.StringValue() {
|
||||||
authtype, ok := commentCtx.Map()["auth_type"]
|
|
||||||
if ok && (authtype == authtypes.IdentNProviderAPIkey.StringValue()) {
|
|
||||||
if err := claims.IsAdmin(); err != nil {
|
if err := claims.IsAdmin(); err != nil {
|
||||||
middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims)
|
middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims)
|
||||||
render.Error(rw, err)
|
render.Error(rw, err)
|
||||||
|
|||||||
@@ -101,13 +101,8 @@ func (provider *provider) GetIdentity(req *http.Request) (*authtypes.Identity, e
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
identity := authtypes.Identity{
|
identity := authtypes.NewIdentity(user.ID, user.OrgID, user.Email, apiKey.Role, provider.Name())
|
||||||
UserID: user.ID,
|
return identity, nil
|
||||||
Role: apiKey.Role,
|
|
||||||
Email: user.Email,
|
|
||||||
OrgID: user.OrgID,
|
|
||||||
}
|
|
||||||
return &identity, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) Post(ctx context.Context, _ *http.Request, _ authtypes.Claims) {
|
func (provider *provider) Post(ctx context.Context, _ *http.Request, _ authtypes.Claims) {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func (m *module) ListPromotedAndIndexedPaths(ctx context.Context) ([]promotetype
|
|||||||
|
|
||||||
// add the paths that are not promoted but have indexes
|
// add the paths that are not promoted but have indexes
|
||||||
for path, indexes := range aggr {
|
for path, indexes := range aggr {
|
||||||
path := strings.TrimPrefix(path, telemetrylogs.BodyJSONColumnPrefix)
|
path := strings.TrimPrefix(path, telemetrylogs.BodyV2ColumnPrefix)
|
||||||
path = telemetrytypes.BodyJSONStringSearchPrefix + path
|
path = telemetrytypes.BodyJSONStringSearchPrefix + path
|
||||||
response = append(response, promotetypes.PromotePath{
|
response = append(response, promotetypes.PromotePath{
|
||||||
Path: path,
|
Path: path,
|
||||||
@@ -163,7 +163,7 @@ func (m *module) PromoteAndIndexPaths(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(it.Indexes) > 0 {
|
if len(it.Indexes) > 0 {
|
||||||
parentColumn := telemetrylogs.LogsV2BodyJSONColumn
|
parentColumn := telemetrylogs.LogsV2BodyV2Column
|
||||||
// if the path is already promoted or is being promoted, add it to the promoted column
|
// if the path is already promoted or is being promoted, add it to the promoted column
|
||||||
if _, promoted := existingPromotedPaths[it.Path]; promoted || it.Promote {
|
if _, promoted := existingPromotedPaths[it.Path]; promoted || it.Promote {
|
||||||
parentColumn = telemetrylogs.LogsV2BodyPromotedColumn
|
parentColumn = telemetrylogs.LogsV2BodyPromotedColumn
|
||||||
|
|||||||
@@ -27,25 +27,6 @@ func NewHandler(module root.Module, getter root.Getter) root.Handler {
|
|||||||
return &handler{module: module, getter: getter}
|
return &handler{module: module, getter: getter}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
req := new(types.PostableAcceptInvite)
|
|
||||||
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := h.module.AcceptInvite(ctx, req.InviteToken, req.Password)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Success(w, http.StatusCreated, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
|
func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -104,59 +85,6 @@ func (h *handler) CreateBulkInvite(rw http.ResponseWriter, r *http.Request) {
|
|||||||
render.Success(rw, http.StatusCreated, nil)
|
render.Success(rw, http.StatusCreated, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) GetInvite(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
token := mux.Vars(r)["token"]
|
|
||||||
invite, err := h.module.GetInviteByToken(ctx, token)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Success(w, http.StatusOK, invite)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handler) ListInvite(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
invites, err := h.module.ListInvite(ctx, claims.OrgID)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Success(w, http.StatusOK, invites)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handler) DeleteInvite(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
id := mux.Vars(r)["id"]
|
|
||||||
|
|
||||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.module.DeleteUser(ctx, valuer.MustNewUUID(claims.OrgID), id, claims.UserID); err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Success(w, http.StatusNoContent, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handler) GetUser(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) GetUser(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -213,9 +141,6 @@ func (h *handler) ListUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// temp code - show only active users
|
|
||||||
users = slices.DeleteFunc(users, func(user *types.User) bool { return user.Status != types.UserStatusActive })
|
|
||||||
|
|
||||||
render.Success(w, http.StatusOK, users)
|
render.Success(w, http.StatusOK, users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,54 +49,6 @@ func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing em
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) AcceptInvite(ctx context.Context, token string, password string) (*types.User, error) {
|
|
||||||
// get the user by reset password token
|
|
||||||
user, err := m.store.GetUserByResetPasswordToken(ctx, token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the password and delete the token
|
|
||||||
err = m.UpdatePasswordByResetPasswordToken(ctx, token, password)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// query the user again
|
|
||||||
user, err = m.store.GetByOrgIDAndID(ctx, user.OrgID, user.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) GetInviteByToken(ctx context.Context, token string) (*types.Invite, error) {
|
|
||||||
// get the user
|
|
||||||
user, err := m.store.GetUserByResetPasswordToken(ctx, token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a dummy invite obj for backward compatibility
|
|
||||||
invite := &types.Invite{
|
|
||||||
Identifiable: types.Identifiable{
|
|
||||||
ID: user.ID,
|
|
||||||
},
|
|
||||||
Name: user.DisplayName,
|
|
||||||
Email: user.Email,
|
|
||||||
Token: token,
|
|
||||||
Role: user.Role,
|
|
||||||
OrgID: user.OrgID,
|
|
||||||
TimeAuditable: types.TimeAuditable{
|
|
||||||
CreatedAt: user.CreatedAt,
|
|
||||||
UpdatedAt: user.UpdatedAt,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return invite, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateBulk implements invite.Module.
|
// CreateBulk implements invite.Module.
|
||||||
func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error) {
|
func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error) {
|
||||||
creator, err := m.store.GetUser(ctx, userID)
|
creator, err := m.store.GetUser(ctx, userID)
|
||||||
@@ -218,46 +170,6 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID
|
|||||||
return invites, nil
|
return invites, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) {
|
|
||||||
// find all the users with pending_invite status
|
|
||||||
users, err := m.store.ListUsersByOrgID(ctx, valuer.MustNewUUID(orgID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingUsers := slices.DeleteFunc(users, func(user *types.User) bool { return user.Status != types.UserStatusPendingInvite })
|
|
||||||
|
|
||||||
var invites []*types.Invite
|
|
||||||
|
|
||||||
for _, pUser := range pendingUsers {
|
|
||||||
// get the reset password token
|
|
||||||
resetPasswordToken, err := m.GetOrCreateResetPasswordToken(ctx, pUser.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a dummy invite obj for backward compatibility
|
|
||||||
invite := &types.Invite{
|
|
||||||
Identifiable: types.Identifiable{
|
|
||||||
ID: pUser.ID,
|
|
||||||
},
|
|
||||||
Name: pUser.DisplayName,
|
|
||||||
Email: pUser.Email,
|
|
||||||
Token: resetPasswordToken.Token,
|
|
||||||
Role: pUser.Role,
|
|
||||||
OrgID: pUser.OrgID,
|
|
||||||
TimeAuditable: types.TimeAuditable{
|
|
||||||
CreatedAt: pUser.CreatedAt,
|
|
||||||
UpdatedAt: pUser.UpdatedAt, // dummy
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
invites = append(invites, invite)
|
|
||||||
}
|
|
||||||
|
|
||||||
return invites, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error {
|
func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error {
|
||||||
createUserOpts := root.NewCreateUserOptions(opts...)
|
createUserOpts := root.NewCreateUserOptions(opts...)
|
||||||
|
|
||||||
@@ -304,10 +216,6 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
|
|||||||
return nil, errors.WithAdditionalf(err, "cannot update deleted user")
|
return nil, errors.WithAdditionalf(err, "cannot update deleted user")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := existingUser.ErrIfPending(); err != nil {
|
|
||||||
return nil, errors.WithAdditionalf(err, "cannot update pending user")
|
|
||||||
}
|
|
||||||
|
|
||||||
requestor, err := m.store.GetUser(ctx, valuer.MustNewUUID(updatedBy))
|
requestor, err := m.store.GetUser(ctx, valuer.MustNewUUID(updatedBy))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -41,9 +41,6 @@ type Module interface {
|
|||||||
|
|
||||||
// invite
|
// invite
|
||||||
CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error)
|
CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error)
|
||||||
ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error)
|
|
||||||
AcceptInvite(ctx context.Context, token string, password string) (*types.User, error)
|
|
||||||
GetInviteByToken(ctx context.Context, token string) (*types.Invite, error)
|
|
||||||
|
|
||||||
// API KEY
|
// API KEY
|
||||||
CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error
|
CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error
|
||||||
@@ -89,10 +86,6 @@ type Getter interface {
|
|||||||
type Handler interface {
|
type Handler interface {
|
||||||
// invite
|
// invite
|
||||||
CreateInvite(http.ResponseWriter, *http.Request)
|
CreateInvite(http.ResponseWriter, *http.Request)
|
||||||
AcceptInvite(http.ResponseWriter, *http.Request)
|
|
||||||
GetInvite(http.ResponseWriter, *http.Request) // public function
|
|
||||||
ListInvite(http.ResponseWriter, *http.Request)
|
|
||||||
DeleteInvite(http.ResponseWriter, *http.Request)
|
|
||||||
CreateBulkInvite(http.ResponseWriter, *http.Request)
|
CreateBulkInvite(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
ListUsers(http.ResponseWriter, *http.Request)
|
ListUsers(http.ResponseWriter, *http.Request)
|
||||||
|
|||||||
@@ -10,13 +10,11 @@ import (
|
|||||||
|
|
||||||
"github.com/ClickHouse/clickhouse-go/v2"
|
"github.com/ClickHouse/clickhouse-go/v2"
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
|
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
|
||||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type builderQuery[T any] struct {
|
type builderQuery[T any] struct {
|
||||||
@@ -262,40 +260,6 @@ func (q *builderQuery[T]) executeWithContext(ctx context.Context, query string,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// merge body_json and promoted into body
|
|
||||||
if q.spec.Signal == telemetrytypes.SignalLogs {
|
|
||||||
switch typedPayload := payload.(type) {
|
|
||||||
case *qbtypes.RawData:
|
|
||||||
for _, rr := range typedPayload.Rows {
|
|
||||||
seeder := func() error {
|
|
||||||
body, ok := rr.Data[telemetrylogs.LogsV2BodyJSONColumn].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
promoted, ok := rr.Data[telemetrylogs.LogsV2BodyPromotedColumn].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
seed(promoted, body)
|
|
||||||
str, err := sonic.MarshalString(body)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal body")
|
|
||||||
}
|
|
||||||
rr.Data["body"] = str
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := seeder()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(rr.Data, telemetrylogs.LogsV2BodyJSONColumn)
|
|
||||||
delete(rr.Data, telemetrylogs.LogsV2BodyPromotedColumn)
|
|
||||||
}
|
|
||||||
payload = typedPayload
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &qbtypes.Result{
|
return &qbtypes.Result{
|
||||||
Type: q.kind,
|
Type: q.kind,
|
||||||
Value: payload,
|
Value: payload,
|
||||||
@@ -423,18 +387,3 @@ func decodeCursor(cur string) (int64, error) {
|
|||||||
}
|
}
|
||||||
return strconv.ParseInt(string(b), 10, 64)
|
return strconv.ParseInt(string(b), 10, 64)
|
||||||
}
|
}
|
||||||
|
|
||||||
func seed(promoted map[string]any, body map[string]any) {
|
|
||||||
for key, fromValue := range promoted {
|
|
||||||
if toValue, ok := body[key]; !ok {
|
|
||||||
body[key] = fromValue
|
|
||||||
} else {
|
|
||||||
if fromValue, ok := fromValue.(map[string]any); ok {
|
|
||||||
if toValue, ok := toValue.(map[string]any); ok {
|
|
||||||
seed(fromValue, toValue)
|
|
||||||
body[key] = toValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -394,17 +393,11 @@ func readAsRaw(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
|
|||||||
|
|
||||||
// de-reference the typed pointer to any
|
// de-reference the typed pointer to any
|
||||||
val := reflect.ValueOf(cellPtr).Elem().Interface()
|
val := reflect.ValueOf(cellPtr).Elem().Interface()
|
||||||
|
// Post-process JSON columns: normalize into String value
|
||||||
// Post-process JSON columns: normalize into structured values
|
|
||||||
if strings.HasPrefix(strings.ToUpper(colTypes[i].DatabaseTypeName()), "JSON") {
|
if strings.HasPrefix(strings.ToUpper(colTypes[i].DatabaseTypeName()), "JSON") {
|
||||||
switch x := val.(type) {
|
switch x := val.(type) {
|
||||||
case []byte:
|
case []byte:
|
||||||
if len(x) > 0 {
|
val = string(x)
|
||||||
var v any
|
|
||||||
if err := sonic.Unmarshal(x, &v); err == nil {
|
|
||||||
val = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
// already a structured type (map[string]any, []any, etc.)
|
// already a structured type (map[string]any, []any, etc.)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ const SpanSearchScopeRoot = "isroot"
|
|||||||
const SpanSearchScopeEntryPoint = "isentrypoint"
|
const SpanSearchScopeEntryPoint = "isentrypoint"
|
||||||
const OrderBySpanCount = "span_count"
|
const OrderBySpanCount = "span_count"
|
||||||
|
|
||||||
// Deprecated: Use the new emailing service instead
|
|
||||||
var InviteEmailTemplate = GetOrDefaultEnv("INVITE_EMAIL_TEMPLATE", "/root/templates/invitation_email.gotmpl")
|
|
||||||
|
|
||||||
var MetricsExplorerClickhouseThreads = GetOrDefaultEnvInt("METRICS_EXPLORER_CLICKHOUSE_THREADS", 8)
|
var MetricsExplorerClickhouseThreads = GetOrDefaultEnvInt("METRICS_EXPLORER_CLICKHOUSE_THREADS", 8)
|
||||||
var UpdatedMetricsMetadataCachePrefix = GetOrDefaultEnv("METRICS_UPDATED_METADATA_CACHE_KEY", "UPDATED_METRICS_METADATA")
|
var UpdatedMetricsMetadataCachePrefix = GetOrDefaultEnv("METRICS_UPDATED_METADATA_CACHE_KEY", "UPDATED_METRICS_METADATA")
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
opentracing "github.com/opentracing/opentracing-go"
|
opentracing "github.com/opentracing/opentracing-go"
|
||||||
plabels "github.com/prometheus/prometheus/model/labels"
|
plabels "github.com/prometheus/prometheus/model/labels"
|
||||||
"log/slog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PromRuleTask is a promql rule executor
|
// PromRuleTask is a promql rule executor
|
||||||
@@ -371,7 +373,7 @@ func (g *PromRuleTask) Eval(ctx context.Context, ts time.Time) {
|
|||||||
|
|
||||||
comment := ctxtypes.CommentFromContext(ctx)
|
comment := ctxtypes.CommentFromContext(ctx)
|
||||||
comment.Set("rule_id", rule.ID())
|
comment.Set("rule_id", rule.ID())
|
||||||
comment.Set("auth_type", "internal")
|
comment.Set("identn_provider", authtypes.IdentNProviderInternal.StringValue())
|
||||||
ctx = ctxtypes.NewContextWithComment(ctx, comment)
|
ctx = ctxtypes.NewContextWithComment(ctx, comment)
|
||||||
|
|
||||||
_, err := rule.Eval(ctx, ts)
|
_, err := rule.Eval(ctx, ts)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
@@ -358,7 +359,7 @@ func (g *RuleTask) Eval(ctx context.Context, ts time.Time) {
|
|||||||
|
|
||||||
comment := ctxtypes.CommentFromContext(ctx)
|
comment := ctxtypes.CommentFromContext(ctx)
|
||||||
comment.Set("rule_id", rule.ID())
|
comment.Set("rule_id", rule.ID())
|
||||||
comment.Set("auth_type", "internal")
|
comment.Set("identn_provider", authtypes.IdentNProviderInternal.StringValue())
|
||||||
ctx = ctxtypes.NewContextWithComment(ctx, comment)
|
ctx = ctxtypes.NewContextWithComment(ctx, comment)
|
||||||
|
|
||||||
_, err := rule.Eval(ctx, ts)
|
_, err := rule.Eval(ctx, ts)
|
||||||
|
|||||||
@@ -219,7 +219,6 @@ func DataTypeCollisionHandledFieldName(key *telemetrytypes.TelemetryFieldKey, va
|
|||||||
// we don't have a toBoolOrNull in ClickHouse, so we need to convert the bool to a string
|
// we don't have a toBoolOrNull in ClickHouse, so we need to convert the bool to a string
|
||||||
value = fmt.Sprintf("%t", v)
|
value = fmt.Sprintf("%t", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
case telemetrytypes.FieldDataTypeInt64,
|
case telemetrytypes.FieldDataTypeInt64,
|
||||||
telemetrytypes.FieldDataTypeArrayInt64,
|
telemetrytypes.FieldDataTypeArrayInt64,
|
||||||
telemetrytypes.FieldDataTypeNumber,
|
telemetrytypes.FieldDataTypeNumber,
|
||||||
|
|||||||
@@ -313,37 +313,30 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
child := ctx.GetChild(0)
|
child := ctx.GetChild(0)
|
||||||
|
var searchText string
|
||||||
if keyCtx, ok := child.(*grammar.KeyContext); ok {
|
if keyCtx, ok := child.(*grammar.KeyContext); ok {
|
||||||
// create a full text search condition on the body field
|
// create a full text search condition on the body field
|
||||||
|
searchText = keyCtx.GetText()
|
||||||
keyText := keyCtx.GetText()
|
|
||||||
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(keyText), v.builder, v.startNs, v.endNs)
|
|
||||||
if err != nil {
|
|
||||||
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return cond
|
|
||||||
} else if valCtx, ok := child.(*grammar.ValueContext); ok {
|
} else if valCtx, ok := child.(*grammar.ValueContext); ok {
|
||||||
var text string
|
|
||||||
if valCtx.QUOTED_TEXT() != nil {
|
if valCtx.QUOTED_TEXT() != nil {
|
||||||
text = trimQuotes(valCtx.QUOTED_TEXT().GetText())
|
searchText = trimQuotes(valCtx.QUOTED_TEXT().GetText())
|
||||||
} else if valCtx.NUMBER() != nil {
|
} else if valCtx.NUMBER() != nil {
|
||||||
text = valCtx.NUMBER().GetText()
|
searchText = valCtx.NUMBER().GetText()
|
||||||
} else if valCtx.BOOL() != nil {
|
} else if valCtx.BOOL() != nil {
|
||||||
text = valCtx.BOOL().GetText()
|
searchText = valCtx.BOOL().GetText()
|
||||||
} else if valCtx.KEY() != nil {
|
} else if valCtx.KEY() != nil {
|
||||||
text = valCtx.KEY().GetText()
|
searchText = valCtx.KEY().GetText()
|
||||||
} else {
|
} else {
|
||||||
v.errors = append(v.errors, fmt.Sprintf("unsupported value type: %s", valCtx.GetText()))
|
v.errors = append(v.errors, fmt.Sprintf("unsupported value type: %s", valCtx.GetText()))
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(text), v.builder, v.startNs, v.endNs)
|
|
||||||
if err != nil {
|
|
||||||
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return cond
|
|
||||||
}
|
}
|
||||||
|
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(searchText), v.builder, v.startNs, v.endNs)
|
||||||
|
if err != nil {
|
||||||
|
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
return "" // Should not happen with valid input
|
return "" // Should not happen with valid input
|
||||||
@@ -383,6 +376,7 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
|
|||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
condition, err := v.conditionBuilder.ConditionFor(context.Background(), key, op, nil, v.builder, v.startNs, v.endNs)
|
condition, err := v.conditionBuilder.ConditionFor(context.Background(), key, op, nil, v.builder, v.startNs, v.endNs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
v.errors = append(v.errors, fmt.Sprintf("failed to build condition: %s", err.Error()))
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
conds = append(conds, condition)
|
conds = append(conds, condition)
|
||||||
@@ -648,7 +642,6 @@ func (v *filterExpressionVisitor) VisitValueList(ctx *grammar.ValueListContext)
|
|||||||
|
|
||||||
// VisitFullText handles standalone quoted strings for full-text search
|
// VisitFullText handles standalone quoted strings for full-text search
|
||||||
func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) any {
|
func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) any {
|
||||||
|
|
||||||
if v.skipFullTextFilter {
|
if v.skipFullTextFilter {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -670,6 +663,7 @@ func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) an
|
|||||||
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
|
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ package telemetrylogs
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
|
|
||||||
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
|
|
||||||
"github.com/huandu/go-sqlbuilder"
|
"github.com/huandu/go-sqlbuilder"
|
||||||
)
|
)
|
||||||
@@ -35,7 +33,7 @@ func (c *conditionBuilder) conditionFor(
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if column.Type.GetType() == schema.ColumnTypeEnumJSON && querybuilder.BodyJSONQueryEnabled {
|
if column.Type.GetType() == schema.ColumnTypeEnumJSON && querybuilder.BodyJSONQueryEnabled && key.Name != messageSubField {
|
||||||
valueType, value := InferDataType(value, operator, key)
|
valueType, value := InferDataType(value, operator, key)
|
||||||
cond, err := NewJSONConditionBuilder(key, valueType).buildJSONCondition(operator, value, sb)
|
cond, err := NewJSONConditionBuilder(key, valueType).buildJSONCondition(operator, value, sb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -54,14 +52,14 @@ func (c *conditionBuilder) conditionFor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a body JSON search - either by FieldContext
|
// Check if this is a body JSON search - either by FieldContext
|
||||||
if key.FieldContext == telemetrytypes.FieldContextBody {
|
if key.FieldContext == telemetrytypes.FieldContextBody && !querybuilder.BodyJSONQueryEnabled {
|
||||||
tblFieldName, value = GetBodyJSONKey(ctx, key, operator, value)
|
tblFieldName, value = GetBodyJSONKey(ctx, key, operator, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
tblFieldName, value = querybuilder.DataTypeCollisionHandledFieldName(key, value, tblFieldName, operator)
|
tblFieldName, value = querybuilder.DataTypeCollisionHandledFieldName(key, value, tblFieldName, operator)
|
||||||
|
|
||||||
// make use of case insensitive index for body
|
// make use of case insensitive index for body
|
||||||
if tblFieldName == "body" {
|
if tblFieldName == "body" || tblFieldName == messageSubColumn {
|
||||||
switch operator {
|
switch operator {
|
||||||
case qbtypes.FilterOperatorLike:
|
case qbtypes.FilterOperatorLike:
|
||||||
return sb.ILike(tblFieldName, value), nil
|
return sb.ILike(tblFieldName, value), nil
|
||||||
@@ -108,7 +106,6 @@ func (c *conditionBuilder) conditionFor(
|
|||||||
return sb.ILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
|
return sb.ILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
|
||||||
case qbtypes.FilterOperatorNotContains:
|
case qbtypes.FilterOperatorNotContains:
|
||||||
return sb.NotILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
|
return sb.NotILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
|
||||||
|
|
||||||
case qbtypes.FilterOperatorRegexp:
|
case qbtypes.FilterOperatorRegexp:
|
||||||
// Note: Escape $$ to $$$$ to avoid sqlbuilder interpreting materialized $ signs
|
// Note: Escape $$ to $$$$ to avoid sqlbuilder interpreting materialized $ signs
|
||||||
// Only needed because we are using sprintf instead of sb.Match (not implemented in sqlbuilder)
|
// Only needed because we are using sprintf instead of sb.Match (not implemented in sqlbuilder)
|
||||||
@@ -178,9 +175,8 @@ func (c *conditionBuilder) conditionFor(
|
|||||||
case schema.ColumnTypeEnumJSON:
|
case schema.ColumnTypeEnumJSON:
|
||||||
if operator == qbtypes.FilterOperatorExists {
|
if operator == qbtypes.FilterOperatorExists {
|
||||||
return sb.IsNotNull(tblFieldName), nil
|
return sb.IsNotNull(tblFieldName), nil
|
||||||
} else {
|
|
||||||
return sb.IsNull(tblFieldName), nil
|
|
||||||
}
|
}
|
||||||
|
return sb.IsNull(tblFieldName), nil
|
||||||
case schema.ColumnTypeEnumLowCardinality:
|
case schema.ColumnTypeEnumLowCardinality:
|
||||||
switch elementType := column.Type.(schema.LowCardinalityColumnType).ElementType; elementType.GetType() {
|
switch elementType := column.Type.(schema.LowCardinalityColumnType).ElementType; elementType.GetType() {
|
||||||
case schema.ColumnTypeEnumString:
|
case schema.ColumnTypeEnumString:
|
||||||
@@ -247,19 +243,30 @@ func (c *conditionBuilder) ConditionFor(
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(key.FieldContext == telemetrytypes.FieldContextBody && querybuilder.BodyJSONQueryEnabled) && operator.AddDefaultExistsFilter() {
|
// Skip adding exists filter for intrinsic fields i.e. Table level log context fields
|
||||||
// skip adding exists filter for intrinsic fields
|
buildExistCondition := operator.AddDefaultExistsFilter()
|
||||||
// with an exception for body json search
|
switch key.FieldContext {
|
||||||
field, _ := c.fm.FieldFor(ctx, key)
|
case telemetrytypes.FieldContextLog, telemetrytypes.FieldContextScope:
|
||||||
if slices.Contains(maps.Keys(IntrinsicFields), field) && key.FieldContext != telemetrytypes.FieldContextBody {
|
// pass; No need to build exist condition for top level columns
|
||||||
|
// immediately return
|
||||||
|
return condition, nil
|
||||||
|
case telemetrytypes.FieldContextResource, telemetrytypes.FieldContextAttribute:
|
||||||
|
// build exist condition for resource and attribute fields based on filter operator
|
||||||
|
case telemetrytypes.FieldContextBody:
|
||||||
|
// Querying JSON fields already account for Nullability of fields
|
||||||
|
// so additional exists checks are not needed
|
||||||
|
if querybuilder.BodyJSONQueryEnabled {
|
||||||
return condition, nil
|
return condition, nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if buildExistCondition {
|
||||||
existsCondition, err := c.conditionFor(ctx, key, qbtypes.FilterOperatorExists, nil, sb)
|
existsCondition, err := c.conditionFor(ctx, key, qbtypes.FilterOperatorExists, nil, sb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return sb.And(condition, existsCondition), nil
|
return sb.And(condition, existsCondition), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return condition, nil
|
return condition, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,8 @@ func TestConditionFor(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Contains operator - body",
|
name: "Contains operator - body",
|
||||||
key: telemetrytypes.TelemetryFieldKey{
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
Name: "body",
|
Name: "body",
|
||||||
|
FieldContext: telemetrytypes.FieldContextLog,
|
||||||
},
|
},
|
||||||
operator: qbtypes.FilterOperatorContains,
|
operator: qbtypes.FilterOperatorContains,
|
||||||
value: 521509198310,
|
value: 521509198310,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package telemetrylogs
|
package telemetrylogs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz-otel-collector/constants"
|
"github.com/SigNoz/signoz-otel-collector/constants"
|
||||||
|
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
)
|
)
|
||||||
@@ -17,7 +20,7 @@ const (
|
|||||||
LogsV2TimestampColumn = "timestamp"
|
LogsV2TimestampColumn = "timestamp"
|
||||||
LogsV2ObservedTimestampColumn = "observed_timestamp"
|
LogsV2ObservedTimestampColumn = "observed_timestamp"
|
||||||
LogsV2BodyColumn = "body"
|
LogsV2BodyColumn = "body"
|
||||||
LogsV2BodyJSONColumn = constants.BodyV2Column
|
LogsV2BodyV2Column = constants.BodyV2Column
|
||||||
LogsV2BodyPromotedColumn = constants.BodyPromotedColumn
|
LogsV2BodyPromotedColumn = constants.BodyPromotedColumn
|
||||||
LogsV2TraceIDColumn = "trace_id"
|
LogsV2TraceIDColumn = "trace_id"
|
||||||
LogsV2SpanIDColumn = "span_id"
|
LogsV2SpanIDColumn = "span_id"
|
||||||
@@ -34,8 +37,14 @@ const (
|
|||||||
LogsV2ResourcesStringColumn = "resources_string"
|
LogsV2ResourcesStringColumn = "resources_string"
|
||||||
LogsV2ScopeStringColumn = "scope_string"
|
LogsV2ScopeStringColumn = "scope_string"
|
||||||
|
|
||||||
BodyJSONColumnPrefix = constants.BodyV2ColumnPrefix
|
BodyV2ColumnPrefix = constants.BodyV2ColumnPrefix
|
||||||
BodyPromotedColumnPrefix = constants.BodyPromotedColumnPrefix
|
BodyPromotedColumnPrefix = constants.BodyPromotedColumnPrefix
|
||||||
|
|
||||||
|
// messageSubColumn is the ClickHouse sub-column that body searches map to
|
||||||
|
// when BodyJSONQueryEnabled is true.
|
||||||
|
messageSubField = "message"
|
||||||
|
messageSubColumn = "body_v2.message"
|
||||||
|
bodySearchDefaultWarning = "body searches default to `body.message:string`. Use `body.<key>` to search a different field inside body"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -118,3 +127,11 @@ var (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func bodyAliasExpression() string {
|
||||||
|
if !querybuilder.BodyJSONQueryEnabled {
|
||||||
|
return LogsV2BodyColumn
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s as body", LogsV2BodyV2Column)
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ var (
|
|||||||
"severity_text": {Name: "severity_text", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}},
|
"severity_text": {Name: "severity_text", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}},
|
||||||
"severity_number": {Name: "severity_number", Type: schema.ColumnTypeUInt8},
|
"severity_number": {Name: "severity_number", Type: schema.ColumnTypeUInt8},
|
||||||
"body": {Name: "body", Type: schema.ColumnTypeString},
|
"body": {Name: "body", Type: schema.ColumnTypeString},
|
||||||
LogsV2BodyJSONColumn: {Name: LogsV2BodyJSONColumn, Type: schema.JSONColumnType{
|
messageSubColumn: {Name: messageSubColumn, Type: schema.ColumnTypeString},
|
||||||
|
LogsV2BodyV2Column: {Name: LogsV2BodyV2Column, Type: schema.JSONColumnType{
|
||||||
MaxDynamicTypes: utils.ToPointer(uint(32)),
|
MaxDynamicTypes: utils.ToPointer(uint(32)),
|
||||||
MaxDynamicPaths: utils.ToPointer(uint(0)),
|
MaxDynamicPaths: utils.ToPointer(uint(0)),
|
||||||
}},
|
}},
|
||||||
@@ -88,21 +89,26 @@ func (m *fieldMapper) getColumn(_ context.Context, key *telemetrytypes.Telemetry
|
|||||||
return logsV2Columns["attributes_bool"], nil
|
return logsV2Columns["attributes_bool"], nil
|
||||||
}
|
}
|
||||||
case telemetrytypes.FieldContextBody:
|
case telemetrytypes.FieldContextBody:
|
||||||
// Body context is for JSON body fields
|
// Body context is for JSON body fields. Use body_v2 if feature flag is enabled.
|
||||||
// Use body_json if feature flag is enabled
|
|
||||||
if querybuilder.BodyJSONQueryEnabled {
|
if querybuilder.BodyJSONQueryEnabled {
|
||||||
return logsV2Columns[LogsV2BodyJSONColumn], nil
|
if key.Name == messageSubField {
|
||||||
|
return logsV2Columns[messageSubColumn], nil
|
||||||
|
}
|
||||||
|
return logsV2Columns[LogsV2BodyV2Column], nil
|
||||||
}
|
}
|
||||||
// Fall back to legacy body column
|
// Fall back to legacy body column
|
||||||
return logsV2Columns["body"], nil
|
return logsV2Columns["body"], nil
|
||||||
case telemetrytypes.FieldContextLog, telemetrytypes.FieldContextUnspecified:
|
case telemetrytypes.FieldContextLog, telemetrytypes.FieldContextUnspecified:
|
||||||
|
if key.Name == LogsV2BodyColumn && querybuilder.BodyJSONQueryEnabled {
|
||||||
|
return logsV2Columns[messageSubColumn], nil
|
||||||
|
}
|
||||||
col, ok := logsV2Columns[key.Name]
|
col, ok := logsV2Columns[key.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
// check if the key has body JSON search
|
// check if the key has body JSON search
|
||||||
if strings.HasPrefix(key.Name, telemetrytypes.BodyJSONStringSearchPrefix) {
|
if strings.HasPrefix(key.Name, telemetrytypes.BodyJSONStringSearchPrefix) {
|
||||||
// Use body_json if feature flag is enabled and we have a body condition builder
|
// Use body_v2 if feature flag is enabled and we have a body condition builder
|
||||||
if querybuilder.BodyJSONQueryEnabled {
|
if querybuilder.BodyJSONQueryEnabled {
|
||||||
return logsV2Columns[LogsV2BodyJSONColumn], nil
|
return logsV2Columns[LogsV2BodyV2Column], nil
|
||||||
}
|
}
|
||||||
// Fall back to legacy body column
|
// Fall back to legacy body column
|
||||||
return logsV2Columns["body"], nil
|
return logsV2Columns["body"], nil
|
||||||
@@ -138,6 +144,10 @@ func (m *fieldMapper) FieldFor(ctx context.Context, key *telemetrytypes.Telemetr
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, mapContains(%s, '%s'), %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldColumn.Name, key.Name, oldKeyName), nil
|
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, mapContains(%s, '%s'), %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldColumn.Name, key.Name, oldKeyName), nil
|
||||||
case telemetrytypes.FieldContextBody:
|
case telemetrytypes.FieldContextBody:
|
||||||
|
if key.Name == messageSubField {
|
||||||
|
return messageSubColumn, nil
|
||||||
|
}
|
||||||
|
|
||||||
if key.JSONDataType == nil {
|
if key.JSONDataType == nil {
|
||||||
return "", qbtypes.ErrColumnNotFound
|
return "", qbtypes.ErrColumnNotFound
|
||||||
}
|
}
|
||||||
@@ -246,34 +256,37 @@ func (m *fieldMapper) buildFieldForJSON(key *telemetrytypes.TelemetryFieldKey) (
|
|||||||
node := plan[0]
|
node := plan[0]
|
||||||
|
|
||||||
expr := fmt.Sprintf("dynamicElement(%s, '%s')", node.FieldPath(), node.TerminalConfig.ElemType.StringValue())
|
expr := fmt.Sprintf("dynamicElement(%s, '%s')", node.FieldPath(), node.TerminalConfig.ElemType.StringValue())
|
||||||
if key.Materialized {
|
// TODO(Piyush): Promoted path logic commented out. Materialized now means type hint
|
||||||
if len(plan) < 2 {
|
// promotion will be extracted from key field evolution
|
||||||
return "", errors.Newf(errors.TypeUnexpected, CodePromotedPlanMissing,
|
// (direct sub-column access), not a promoted body_promoted.* column.
|
||||||
"plan length is less than 2 for promoted path: %s", key.Name)
|
// if key.Materialized {
|
||||||
}
|
// if len(plan) < 2 {
|
||||||
|
// return "", errors.Newf(errors.TypeUnexpected, CodePromotedPlanMissing,
|
||||||
|
// "plan length is less than 2 for promoted path: %s", key.Name)
|
||||||
|
// }
|
||||||
|
|
||||||
node := plan[1]
|
// node := plan[1]
|
||||||
promotedExpr := fmt.Sprintf(
|
// promotedExpr := fmt.Sprintf(
|
||||||
"dynamicElement(%s, '%s')",
|
// "dynamicElement(%s, '%s')",
|
||||||
node.FieldPath(),
|
// node.FieldPath(),
|
||||||
node.TerminalConfig.ElemType.StringValue(),
|
// node.TerminalConfig.ElemType.StringValue(),
|
||||||
)
|
// )
|
||||||
|
|
||||||
// dynamicElement returns NULL for scalar types or an empty array for array types.
|
// // dynamicElement returns NULL for scalar types or an empty array for array types.
|
||||||
if node.TerminalConfig.ElemType.IsArray {
|
// if node.TerminalConfig.ElemType.IsArray {
|
||||||
expr = fmt.Sprintf(
|
// expr = fmt.Sprintf(
|
||||||
"if(length(%s) > 0, %s, %s)",
|
// "if(length(%s) > 0, %s, %s)",
|
||||||
promotedExpr,
|
// promotedExpr,
|
||||||
promotedExpr,
|
// promotedExpr,
|
||||||
expr,
|
// expr,
|
||||||
)
|
// )
|
||||||
} else {
|
// } else {
|
||||||
// promoted column first then body_json column
|
// // promoted column first then body_json column
|
||||||
// TODO(Piyush): Change this in future for better performance
|
// // TODO(Piyush): Change this in future for better performance
|
||||||
expr = fmt.Sprintf("coalesce(%s, %s)", promotedExpr, expr)
|
// expr = fmt.Sprintf("coalesce(%s, %s)", promotedExpr, expr)
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
// }
|
||||||
|
|
||||||
return expr, nil
|
return expr, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func NewJSONConditionBuilder(key *telemetrytypes.TelemetryFieldKey, valueType te
|
|||||||
return &jsonConditionBuilder{key: key, valueType: telemetrytypes.MappingFieldDataTypeToJSONDataType[valueType]}
|
return &jsonConditionBuilder{key: key, valueType: telemetrytypes.MappingFieldDataTypeToJSONDataType[valueType]}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildCondition builds the full WHERE condition for body_json JSON paths
|
// BuildCondition builds the full WHERE condition for body_v2 JSON paths
|
||||||
func (c *jsonConditionBuilder) buildJSONCondition(operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, error) {
|
func (c *jsonConditionBuilder) buildJSONCondition(operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, error) {
|
||||||
conditions := []string{}
|
conditions := []string{}
|
||||||
for _, node := range c.key.JSONPlan {
|
for _, node := range c.key.JSONPlan {
|
||||||
@@ -40,6 +40,7 @@ func (c *jsonConditionBuilder) buildJSONCondition(operator qbtypes.FilterOperato
|
|||||||
}
|
}
|
||||||
conditions = append(conditions, condition)
|
conditions = append(conditions, condition)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.Or(conditions...), nil
|
return sb.Or(conditions...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,9 +289,9 @@ func (c *jsonConditionBuilder) applyOperator(sb *sqlbuilder.SelectBuilder, field
|
|||||||
}
|
}
|
||||||
return sb.NotIn(fieldExpr, values...), nil
|
return sb.NotIn(fieldExpr, values...), nil
|
||||||
case qbtypes.FilterOperatorExists:
|
case qbtypes.FilterOperatorExists:
|
||||||
return fmt.Sprintf("%s IS NOT NULL", fieldExpr), nil
|
return sb.IsNotNull(fieldExpr), nil
|
||||||
case qbtypes.FilterOperatorNotExists:
|
case qbtypes.FilterOperatorNotExists:
|
||||||
return fmt.Sprintf("%s IS NULL", fieldExpr), nil
|
return sb.IsNull(fieldExpr), nil
|
||||||
// between and not between
|
// between and not between
|
||||||
case qbtypes.FilterOperatorBetween, qbtypes.FilterOperatorNotBetween:
|
case qbtypes.FilterOperatorBetween, qbtypes.FilterOperatorNotBetween:
|
||||||
values, ok := value.([]any)
|
values, ok := value.([]any)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -65,7 +65,7 @@ func (b *logQueryStatementBuilder) Build(
|
|||||||
start = querybuilder.ToNanoSecs(start)
|
start = querybuilder.ToNanoSecs(start)
|
||||||
end = querybuilder.ToNanoSecs(end)
|
end = querybuilder.ToNanoSecs(end)
|
||||||
|
|
||||||
keySelectors := getKeySelectors(query)
|
keySelectors, warnings := getKeySelectors(query)
|
||||||
keys, _, err := b.metadataStore.GetKeysMulti(ctx, keySelectors)
|
keys, _, err := b.metadataStore.GetKeysMulti(ctx, keySelectors)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -76,20 +76,29 @@ func (b *logQueryStatementBuilder) Build(
|
|||||||
// Create SQL builder
|
// Create SQL builder
|
||||||
q := sqlbuilder.NewSelectBuilder()
|
q := sqlbuilder.NewSelectBuilder()
|
||||||
|
|
||||||
|
var stmt *qbtypes.Statement
|
||||||
switch requestType {
|
switch requestType {
|
||||||
case qbtypes.RequestTypeRaw, qbtypes.RequestTypeRawStream:
|
case qbtypes.RequestTypeRaw, qbtypes.RequestTypeRawStream:
|
||||||
return b.buildListQuery(ctx, q, query, start, end, keys, variables)
|
stmt, err = b.buildListQuery(ctx, q, query, start, end, keys, variables)
|
||||||
case qbtypes.RequestTypeTimeSeries:
|
case qbtypes.RequestTypeTimeSeries:
|
||||||
return b.buildTimeSeriesQuery(ctx, q, query, start, end, keys, variables)
|
stmt, err = b.buildTimeSeriesQuery(ctx, q, query, start, end, keys, variables)
|
||||||
case qbtypes.RequestTypeScalar:
|
case qbtypes.RequestTypeScalar:
|
||||||
return b.buildScalarQuery(ctx, q, query, start, end, keys, false, variables)
|
stmt, err = b.buildScalarQuery(ctx, q, query, start, end, keys, false, variables)
|
||||||
|
default:
|
||||||
|
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported request type: %s", requestType)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported request type: %s", requestType)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt.Warnings = append(stmt.Warnings, warnings...)
|
||||||
|
return stmt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getKeySelectors(query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]) []*telemetrytypes.FieldKeySelector {
|
func getKeySelectors(query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]) ([]*telemetrytypes.FieldKeySelector, []string) {
|
||||||
var keySelectors []*telemetrytypes.FieldKeySelector
|
var keySelectors []*telemetrytypes.FieldKeySelector
|
||||||
|
var warnings []string
|
||||||
|
|
||||||
for idx := range query.Aggregations {
|
for idx := range query.Aggregations {
|
||||||
aggExpr := query.Aggregations[idx]
|
aggExpr := query.Aggregations[idx]
|
||||||
@@ -136,7 +145,19 @@ func getKeySelectors(query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]) []
|
|||||||
keySelectors[idx].SelectorMatchType = telemetrytypes.FieldSelectorMatchTypeExact
|
keySelectors[idx].SelectorMatchType = telemetrytypes.FieldSelectorMatchTypeExact
|
||||||
}
|
}
|
||||||
|
|
||||||
return keySelectors
|
// When the new JSON body experience is enabled, warn the user if they use the bare
|
||||||
|
// "body" key in the filter — queries on plain "body" default to body.message:string.
|
||||||
|
// TODO(Piyush): Setup better for coming FTS support.
|
||||||
|
if querybuilder.BodyJSONQueryEnabled {
|
||||||
|
for _, sel := range keySelectors {
|
||||||
|
if sel.Name == LogsV2BodyColumn {
|
||||||
|
warnings = append(warnings, bodySearchDefaultWarning)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keySelectors, warnings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *logQueryStatementBuilder) adjustKeys(ctx context.Context, keys map[string][]*telemetrytypes.TelemetryFieldKey, query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation], requestType qbtypes.RequestType) qbtypes.QueryBuilderQuery[qbtypes.LogAggregation] {
|
func (b *logQueryStatementBuilder) adjustKeys(ctx context.Context, keys map[string][]*telemetrytypes.TelemetryFieldKey, query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation], requestType qbtypes.RequestType) qbtypes.QueryBuilderQuery[qbtypes.LogAggregation] {
|
||||||
@@ -203,7 +224,6 @@ func (b *logQueryStatementBuilder) adjustKeys(ctx context.Context, keys map[stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *logQueryStatementBuilder) adjustKey(key *telemetrytypes.TelemetryFieldKey, keys map[string][]*telemetrytypes.TelemetryFieldKey) []string {
|
func (b *logQueryStatementBuilder) adjustKey(key *telemetrytypes.TelemetryFieldKey, keys map[string][]*telemetrytypes.TelemetryFieldKey) []string {
|
||||||
|
|
||||||
// First check if it matches with any intrinsic fields
|
// First check if it matches with any intrinsic fields
|
||||||
var intrinsicOrCalculatedField telemetrytypes.TelemetryFieldKey
|
var intrinsicOrCalculatedField telemetrytypes.TelemetryFieldKey
|
||||||
if _, ok := IntrinsicFields[key.Name]; ok {
|
if _, ok := IntrinsicFields[key.Name]; ok {
|
||||||
@@ -212,7 +232,6 @@ func (b *logQueryStatementBuilder) adjustKey(key *telemetrytypes.TelemetryFieldK
|
|||||||
}
|
}
|
||||||
|
|
||||||
return querybuilder.AdjustKey(key, keys, nil)
|
return querybuilder.AdjustKey(key, keys, nil)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildListQuery builds a query for list panel type
|
// buildListQuery builds a query for list panel type
|
||||||
@@ -249,11 +268,7 @@ func (b *logQueryStatementBuilder) buildListQuery(
|
|||||||
sb.SelectMore(LogsV2SeverityNumberColumn)
|
sb.SelectMore(LogsV2SeverityNumberColumn)
|
||||||
sb.SelectMore(LogsV2ScopeNameColumn)
|
sb.SelectMore(LogsV2ScopeNameColumn)
|
||||||
sb.SelectMore(LogsV2ScopeVersionColumn)
|
sb.SelectMore(LogsV2ScopeVersionColumn)
|
||||||
sb.SelectMore(LogsV2BodyColumn)
|
sb.SelectMore(bodyAliasExpression())
|
||||||
if querybuilder.BodyJSONQueryEnabled {
|
|
||||||
sb.SelectMore(LogsV2BodyJSONColumn)
|
|
||||||
sb.SelectMore(LogsV2BodyPromotedColumn)
|
|
||||||
}
|
|
||||||
sb.SelectMore(LogsV2AttributesStringColumn)
|
sb.SelectMore(LogsV2AttributesStringColumn)
|
||||||
sb.SelectMore(LogsV2AttributesNumberColumn)
|
sb.SelectMore(LogsV2AttributesNumberColumn)
|
||||||
sb.SelectMore(LogsV2AttributesBoolColumn)
|
sb.SelectMore(LogsV2AttributesBoolColumn)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||||
"github.com/SigNoz/signoz/pkg/querybuilder/resourcefilter"
|
"github.com/SigNoz/signoz/pkg/querybuilder/resourcefilter"
|
||||||
@@ -886,3 +887,246 @@ func TestAdjustKey(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStmtBuilderBodyField(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
requestType qbtypes.RequestType
|
||||||
|
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
|
||||||
|
enableBodyJSONQuery bool
|
||||||
|
expected qbtypes.Statement
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "body_exists",
|
||||||
|
requestType: qbtypes.RequestTypeRaw,
|
||||||
|
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||||
|
Signal: telemetrytypes.SignalLogs,
|
||||||
|
Filter: &qbtypes.Filter{Expression: "body Exists"},
|
||||||
|
Limit: 10,
|
||||||
|
},
|
||||||
|
enableBodyJSONQuery: true,
|
||||||
|
expected: qbtypes.Statement{
|
||||||
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND body_v2.message <> ? AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||||
|
Args: []any{uint64(1747945619), uint64(1747983448), "", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||||
|
Warnings: []string{bodySearchDefaultWarning},
|
||||||
|
},
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "body_exists_disabled",
|
||||||
|
requestType: qbtypes.RequestTypeRaw,
|
||||||
|
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||||
|
Signal: telemetrytypes.SignalLogs,
|
||||||
|
Filter: &qbtypes.Filter{Expression: "body Exists"},
|
||||||
|
Limit: 10,
|
||||||
|
},
|
||||||
|
enableBodyJSONQuery: false,
|
||||||
|
expected: qbtypes.Statement{
|
||||||
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND body <> ? AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||||
|
Args: []any{uint64(1747945619), uint64(1747983448), "", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||||
|
},
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "body_empty",
|
||||||
|
requestType: qbtypes.RequestTypeRaw,
|
||||||
|
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||||
|
Signal: telemetrytypes.SignalLogs,
|
||||||
|
Filter: &qbtypes.Filter{Expression: "body == ''"},
|
||||||
|
Limit: 10,
|
||||||
|
},
|
||||||
|
enableBodyJSONQuery: true,
|
||||||
|
expected: qbtypes.Statement{
|
||||||
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND body_v2.message = ? AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||||
|
Args: []any{uint64(1747945619), uint64(1747983448), "", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||||
|
Warnings: []string{bodySearchDefaultWarning},
|
||||||
|
},
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "body_empty_disabled",
|
||||||
|
requestType: qbtypes.RequestTypeRaw,
|
||||||
|
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||||
|
Signal: telemetrytypes.SignalLogs,
|
||||||
|
Filter: &qbtypes.Filter{Expression: "body == ''"},
|
||||||
|
Limit: 10,
|
||||||
|
},
|
||||||
|
enableBodyJSONQuery: false,
|
||||||
|
expected: qbtypes.Statement{
|
||||||
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND body = ? AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||||
|
Args: []any{uint64(1747945619), uint64(1747983448), "", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||||
|
},
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "body_contains",
|
||||||
|
requestType: qbtypes.RequestTypeRaw,
|
||||||
|
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||||
|
Signal: telemetrytypes.SignalLogs,
|
||||||
|
Filter: &qbtypes.Filter{Expression: "body CONTAINS 'error'"},
|
||||||
|
Limit: 10,
|
||||||
|
},
|
||||||
|
enableBodyJSONQuery: true,
|
||||||
|
expected: qbtypes.Statement{
|
||||||
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND LOWER(body_v2.message) LIKE LOWER(?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||||
|
Args: []any{uint64(1747945619), uint64(1747983448), "%error%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||||
|
Warnings: []string{bodySearchDefaultWarning},
|
||||||
|
},
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "body_contains_disabled",
|
||||||
|
requestType: qbtypes.RequestTypeRaw,
|
||||||
|
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||||
|
Signal: telemetrytypes.SignalLogs,
|
||||||
|
Filter: &qbtypes.Filter{Expression: "body CONTAINS 'error'"},
|
||||||
|
Limit: 10,
|
||||||
|
},
|
||||||
|
enableBodyJSONQuery: false,
|
||||||
|
expected: qbtypes.Statement{
|
||||||
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND LOWER(body) LIKE LOWER(?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||||
|
Args: []any{uint64(1747945619), uint64(1747983448), "%error%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||||
|
},
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fm := NewFieldMapper()
|
||||||
|
cb := NewConditionBuilder(fm)
|
||||||
|
|
||||||
|
enable, disable := jsonQueryTestUtil(t)
|
||||||
|
defer disable()
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
if c.enableBodyJSONQuery {
|
||||||
|
enable()
|
||||||
|
} else {
|
||||||
|
disable()
|
||||||
|
}
|
||||||
|
// build the key map after enabling/disabling body JSON query
|
||||||
|
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||||
|
for _, field := range IntrinsicFields {
|
||||||
|
f := field
|
||||||
|
mockMetadataStore.KeysMap[field.Name] = append(mockMetadataStore.KeysMap[field.Name], &f)
|
||||||
|
}
|
||||||
|
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil)
|
||||||
|
resourceFilterStmtBuilder := resourceFilterStmtBuilder()
|
||||||
|
statementBuilder := NewLogQueryStatementBuilder(
|
||||||
|
instrumentationtest.New().ToProviderSettings(),
|
||||||
|
mockMetadataStore,
|
||||||
|
fm,
|
||||||
|
cb,
|
||||||
|
resourceFilterStmtBuilder,
|
||||||
|
aggExprRewriter,
|
||||||
|
DefaultFullTextColumn,
|
||||||
|
GetBodyJSONKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
|
||||||
|
if c.expectedErr != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), c.expectedErr.Error())
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
_, _, _, _, _, add := errors.Unwrapb(err)
|
||||||
|
t.Logf("error additionals: %v", add)
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, c.expected.Query, q.Query)
|
||||||
|
require.Equal(t, c.expected.Args, q.Args)
|
||||||
|
require.Equal(t, c.expected.Warnings, q.Warnings)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStmtBuilderBodyFullTextSearch(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
requestType qbtypes.RequestType
|
||||||
|
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
|
||||||
|
enableBodyJSONQuery bool
|
||||||
|
expected qbtypes.Statement
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "body_contains",
|
||||||
|
requestType: qbtypes.RequestTypeRaw,
|
||||||
|
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||||
|
Signal: telemetrytypes.SignalLogs,
|
||||||
|
Filter: &qbtypes.Filter{Expression: "'error'"},
|
||||||
|
Limit: 10,
|
||||||
|
},
|
||||||
|
enableBodyJSONQuery: true,
|
||||||
|
expected: qbtypes.Statement{
|
||||||
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND match(LOWER(body_v2.message), LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||||
|
Args: []any{uint64(1747945619), uint64(1747983448), "error", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||||
|
},
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "body_contains_disabled",
|
||||||
|
requestType: qbtypes.RequestTypeRaw,
|
||||||
|
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||||
|
Signal: telemetrytypes.SignalLogs,
|
||||||
|
Filter: &qbtypes.Filter{Expression: "'error'"},
|
||||||
|
Limit: 10,
|
||||||
|
},
|
||||||
|
enableBodyJSONQuery: false,
|
||||||
|
expected: qbtypes.Statement{
|
||||||
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND match(LOWER(body), LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||||
|
Args: []any{uint64(1747945619), uint64(1747983448), "error", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||||
|
},
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fm := NewFieldMapper()
|
||||||
|
cb := NewConditionBuilder(fm)
|
||||||
|
|
||||||
|
enable, disable := jsonQueryTestUtil(t)
|
||||||
|
defer disable()
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
if c.enableBodyJSONQuery {
|
||||||
|
enable()
|
||||||
|
} else {
|
||||||
|
disable()
|
||||||
|
}
|
||||||
|
// build the key map after enabling/disabling body JSON query
|
||||||
|
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||||
|
for _, field := range IntrinsicFields {
|
||||||
|
f := field
|
||||||
|
mockMetadataStore.KeysMap[field.Name] = append(mockMetadataStore.KeysMap[field.Name], &f)
|
||||||
|
}
|
||||||
|
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil)
|
||||||
|
resourceFilterStmtBuilder := resourceFilterStmtBuilder()
|
||||||
|
statementBuilder := NewLogQueryStatementBuilder(
|
||||||
|
instrumentationtest.New().ToProviderSettings(),
|
||||||
|
mockMetadataStore,
|
||||||
|
fm,
|
||||||
|
cb,
|
||||||
|
resourceFilterStmtBuilder,
|
||||||
|
aggExprRewriter,
|
||||||
|
DefaultFullTextColumn,
|
||||||
|
GetBodyJSONKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
|
||||||
|
if c.expectedErr != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), c.expectedErr.Error())
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
_, _, _, _, _, add := errors.Unwrapb(err)
|
||||||
|
t.Logf("error additionals: %v", add)
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, c.expected.Query, q.Query)
|
||||||
|
require.Equal(t, c.expected.Args, q.Args)
|
||||||
|
require.Equal(t, c.expected.Warnings, q.Warnings)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,13 +27,6 @@ func buildCompleteFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
|
|||||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"body": {
|
|
||||||
{
|
|
||||||
Name: "body",
|
|
||||||
FieldContext: telemetrytypes.FieldContextLog,
|
|
||||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"http.status_code": {
|
"http.status_code": {
|
||||||
{
|
{
|
||||||
Name: "http.status_code",
|
Name: "http.status_code",
|
||||||
@@ -938,6 +931,13 @@ func buildCompleteFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
|
|||||||
Materialized: true,
|
Materialized: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"body": {
|
||||||
|
{
|
||||||
|
Name: "body",
|
||||||
|
FieldContext: telemetrytypes.FieldContextLog,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, keys := range keysMap {
|
for _, keys := range keysMap {
|
||||||
@@ -945,6 +945,7 @@ func buildCompleteFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
|
|||||||
key.Signal = telemetrytypes.SignalLogs
|
key.Signal = telemetrytypes.SignalLogs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return keysMap
|
return keysMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ func (t *telemetryMetaStore) fetchBodyJSONPaths(ctx context.Context,
|
|||||||
instrumentationtypes.CodeNamespace: "metadata",
|
instrumentationtypes.CodeNamespace: "metadata",
|
||||||
instrumentationtypes.CodeFunctionName: "fetchBodyJSONPaths",
|
instrumentationtypes.CodeFunctionName: "fetchBodyJSONPaths",
|
||||||
})
|
})
|
||||||
|
|
||||||
query, args, limit := buildGetBodyJSONPathsQuery(fieldKeySelectors)
|
query, args, limit := buildGetBodyJSONPathsQuery(fieldKeySelectors)
|
||||||
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
|
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -184,7 +185,6 @@ func buildGetBodyJSONPathsQuery(fieldKeySelectors []*telemetrytypes.FieldKeySele
|
|||||||
limit += fieldKeySelector.Limit
|
limit += fieldKeySelector.Limit
|
||||||
}
|
}
|
||||||
sb.Where(sb.Or(orClauses...))
|
sb.Where(sb.Or(orClauses...))
|
||||||
|
|
||||||
// Group by path to get unique paths with aggregated types
|
// Group by path to get unique paths with aggregated types
|
||||||
sb.GroupBy("path")
|
sb.GroupBy("path")
|
||||||
|
|
||||||
@@ -319,7 +319,7 @@ func (t *telemetryMetaStore) ListJSONValues(ctx context.Context, path string, li
|
|||||||
if promoted {
|
if promoted {
|
||||||
path = telemetrylogs.BodyPromotedColumnPrefix + path
|
path = telemetrylogs.BodyPromotedColumnPrefix + path
|
||||||
} else {
|
} else {
|
||||||
path = telemetrylogs.BodyJSONColumnPrefix + path
|
path = telemetrylogs.BodyV2ColumnPrefix + path
|
||||||
}
|
}
|
||||||
|
|
||||||
from := fmt.Sprintf("%s.%s", telemetrylogs.DBName, telemetrylogs.LogsV2TableName)
|
from := fmt.Sprintf("%s.%s", telemetrylogs.DBName, telemetrylogs.LogsV2TableName)
|
||||||
@@ -522,7 +522,7 @@ func (t *telemetryMetaStore) GetPromotedPaths(ctx context.Context, paths ...stri
|
|||||||
// TODO(Piyush): Remove this function
|
// TODO(Piyush): Remove this function
|
||||||
func CleanPathPrefixes(path string) string {
|
func CleanPathPrefixes(path string) string {
|
||||||
path = strings.TrimPrefix(path, telemetrytypes.BodyJSONStringSearchPrefix)
|
path = strings.TrimPrefix(path, telemetrytypes.BodyJSONStringSearchPrefix)
|
||||||
path = strings.TrimPrefix(path, telemetrylogs.BodyJSONColumnPrefix)
|
path = strings.TrimPrefix(path, telemetrylogs.BodyV2ColumnPrefix)
|
||||||
path = strings.TrimPrefix(path, telemetrylogs.BodyPromotedColumnPrefix)
|
path = strings.TrimPrefix(path, telemetrylogs.BodyPromotedColumnPrefix)
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func NewTelemetryMetaStore(
|
|||||||
jsonColumnMetadata: map[telemetrytypes.Signal]map[telemetrytypes.FieldContext]telemetrytypes.JSONColumnMetadata{
|
jsonColumnMetadata: map[telemetrytypes.Signal]map[telemetrytypes.FieldContext]telemetrytypes.JSONColumnMetadata{
|
||||||
telemetrytypes.SignalLogs: {
|
telemetrytypes.SignalLogs: {
|
||||||
telemetrytypes.FieldContextBody: telemetrytypes.JSONColumnMetadata{
|
telemetrytypes.FieldContextBody: telemetrytypes.JSONColumnMetadata{
|
||||||
BaseColumn: telemetrylogs.LogsV2BodyJSONColumn,
|
BaseColumn: telemetrylogs.LogsV2BodyV2Column,
|
||||||
PromotedColumn: telemetrylogs.LogsV2BodyPromotedColumn,
|
PromotedColumn: telemetrylogs.LogsV2BodyPromotedColumn,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ var (
|
|||||||
IdentNProviderTokenizer = IdentNProvider{valuer.NewString("tokenizer")}
|
IdentNProviderTokenizer = IdentNProvider{valuer.NewString("tokenizer")}
|
||||||
IdentNProviderAPIkey = IdentNProvider{valuer.NewString("api_key")}
|
IdentNProviderAPIkey = IdentNProvider{valuer.NewString("api_key")}
|
||||||
IdentNProviderAnonymous = IdentNProvider{valuer.NewString("anonymous")}
|
IdentNProviderAnonymous = IdentNProvider{valuer.NewString("anonymous")}
|
||||||
|
IdentNProviderInternal = IdentNProvider{valuer.NewString("internal")}
|
||||||
)
|
)
|
||||||
|
|
||||||
type IdentNProvider struct{ valuer.String }
|
type IdentNProvider struct{ valuer.String }
|
||||||
|
|||||||
@@ -30,22 +30,6 @@ type Invite struct {
|
|||||||
InviteLink string `bun:"-" json:"inviteLink"`
|
InviteLink string `bun:"-" json:"inviteLink"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InviteEmailData struct {
|
|
||||||
CustomerName string
|
|
||||||
InviterName string
|
|
||||||
InviterEmail string
|
|
||||||
Link string
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostableAcceptInvite struct {
|
|
||||||
DisplayName string `json:"displayName"`
|
|
||||||
InviteToken string `json:"token"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
|
|
||||||
// reference URL to track where the register request is coming from
|
|
||||||
SourceURL string `json:"sourceUrl"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostableInvite struct {
|
type PostableInvite struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Email valuer.Email `json:"email"`
|
Email valuer.Email `json:"email"`
|
||||||
@@ -79,10 +63,6 @@ func (request *PostableBulkInviteRequest) UnmarshalJSON(data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type GettableCreateInviteResponse struct {
|
|
||||||
InviteToken string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewInvite(name string, role Role, orgID valuer.UUID, email valuer.Email) (*Invite, error) {
|
func NewInvite(name string, role Role, orgID valuer.UUID, email valuer.Email) (*Invite, error) {
|
||||||
invite := &Invite{
|
invite := &Invite{
|
||||||
Identifiable: Identifiable{
|
Identifiable: Identifiable{
|
||||||
@@ -101,23 +81,3 @@ func NewInvite(name string, role Role, orgID valuer.UUID, email valuer.Email) (*
|
|||||||
|
|
||||||
return invite, nil
|
return invite, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (request *PostableAcceptInvite) UnmarshalJSON(data []byte) error {
|
|
||||||
type Alias PostableAcceptInvite
|
|
||||||
|
|
||||||
var temp Alias
|
|
||||||
if err := json.Unmarshal(data, &temp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if temp.InviteToken == "" {
|
|
||||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invite token is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !IsPasswordValid(temp.Password) {
|
|
||||||
return ErrInvalidPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
*request = PostableAcceptInvite(temp)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ type JSONAccessNode struct {
|
|||||||
// Node information
|
// Node information
|
||||||
Name string
|
Name string
|
||||||
IsTerminal bool
|
IsTerminal bool
|
||||||
isRoot bool // marked true for only body_json and body_json_promoted
|
isRoot bool // marked true for only body_v2 and body_promoted
|
||||||
|
|
||||||
// Precomputed type information (single source of truth)
|
// Precomputed type information (single source of truth)
|
||||||
AvailableTypes []JSONDataType
|
AvailableTypes []JSONDataType
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package telemetrytypes
|
package telemetrytypes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
otelconstants "github.com/SigNoz/signoz-otel-collector/constants"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
bodyV2Column = otelconstants.BodyV2Column
|
||||||
|
bodyPromotedColumn = otelconstants.BodyPromotedColumn
|
||||||
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Helper Functions for Test Data Creation
|
// Helper Functions for Test Data Creation
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -109,8 +116,8 @@ func TestNode_Alias(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Root node returns name as-is",
|
name: "Root node returns name as-is",
|
||||||
node: NewRootJSONAccessNode("body_json", 32, 0),
|
node: NewRootJSONAccessNode(bodyV2Column, 32, 0),
|
||||||
expected: "body_json",
|
expected: bodyV2Column,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Node without parent returns backticked name",
|
name: "Node without parent returns backticked name",
|
||||||
@@ -124,9 +131,9 @@ func TestNode_Alias(t *testing.T) {
|
|||||||
name: "Node with root parent uses dot separator",
|
name: "Node with root parent uses dot separator",
|
||||||
node: &JSONAccessNode{
|
node: &JSONAccessNode{
|
||||||
Name: "age",
|
Name: "age",
|
||||||
Parent: NewRootJSONAccessNode("body_json", 32, 0),
|
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
|
||||||
},
|
},
|
||||||
expected: "`" + "body_json" + ".age`",
|
expected: "`" + bodyV2Column + ".age`",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Node with non-root parent uses array separator",
|
name: "Node with non-root parent uses array separator",
|
||||||
@@ -134,10 +141,10 @@ func TestNode_Alias(t *testing.T) {
|
|||||||
Name: "name",
|
Name: "name",
|
||||||
Parent: &JSONAccessNode{
|
Parent: &JSONAccessNode{
|
||||||
Name: "education",
|
Name: "education",
|
||||||
Parent: NewRootJSONAccessNode("body_json", 32, 0),
|
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: "`" + "body_json" + ".education[].name`",
|
expected: "`" + bodyV2Column + ".education[].name`",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Nested array path with multiple levels",
|
name: "Nested array path with multiple levels",
|
||||||
@@ -147,11 +154,11 @@ func TestNode_Alias(t *testing.T) {
|
|||||||
Name: "awards",
|
Name: "awards",
|
||||||
Parent: &JSONAccessNode{
|
Parent: &JSONAccessNode{
|
||||||
Name: "education",
|
Name: "education",
|
||||||
Parent: NewRootJSONAccessNode("body_json", 32, 0),
|
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: "`" + "body_json" + ".education[].awards[].type`",
|
expected: "`" + bodyV2Column + ".education[].awards[].type`",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,18 +180,18 @@ func TestNode_FieldPath(t *testing.T) {
|
|||||||
name: "Simple field path from root",
|
name: "Simple field path from root",
|
||||||
node: &JSONAccessNode{
|
node: &JSONAccessNode{
|
||||||
Name: "user",
|
Name: "user",
|
||||||
Parent: NewRootJSONAccessNode("body_json", 32, 0),
|
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
|
||||||
},
|
},
|
||||||
// FieldPath() always wraps the field name in backticks
|
// FieldPath() always wraps the field name in backticks
|
||||||
expected: "body_json" + ".`user`",
|
expected: bodyV2Column + ".`user`",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Field path with backtick-required key",
|
name: "Field path with backtick-required key",
|
||||||
node: &JSONAccessNode{
|
node: &JSONAccessNode{
|
||||||
Name: "user-name", // requires backtick
|
Name: "user-name", // requires backtick
|
||||||
Parent: NewRootJSONAccessNode("body_json", 32, 0),
|
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
|
||||||
},
|
},
|
||||||
expected: "body_json" + ".`user-name`",
|
expected: bodyV2Column + ".`user-name`",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Nested field path",
|
name: "Nested field path",
|
||||||
@@ -192,11 +199,11 @@ func TestNode_FieldPath(t *testing.T) {
|
|||||||
Name: "age",
|
Name: "age",
|
||||||
Parent: &JSONAccessNode{
|
Parent: &JSONAccessNode{
|
||||||
Name: "user",
|
Name: "user",
|
||||||
Parent: NewRootJSONAccessNode("body_json", 32, 0),
|
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// FieldPath() always wraps the field name in backticks
|
// FieldPath() always wraps the field name in backticks
|
||||||
expected: "`" + "body_json" + ".user`.`age`",
|
expected: "`" + bodyV2Column + ".user`.`age`",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Array element field path",
|
name: "Array element field path",
|
||||||
@@ -204,11 +211,11 @@ func TestNode_FieldPath(t *testing.T) {
|
|||||||
Name: "name",
|
Name: "name",
|
||||||
Parent: &JSONAccessNode{
|
Parent: &JSONAccessNode{
|
||||||
Name: "education",
|
Name: "education",
|
||||||
Parent: NewRootJSONAccessNode("body_json", 32, 0),
|
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// FieldPath() always wraps the field name in backticks
|
// FieldPath() always wraps the field name in backticks
|
||||||
expected: "`" + "body_json" + ".education`.`name`",
|
expected: "`" + bodyV2Column + ".education`.`name`",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,36 +243,36 @@ func TestPlanJSON_BasicStructure(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Simple path not promoted",
|
name: "Simple path not promoted",
|
||||||
key: makeKey("user.name", String, false),
|
key: makeKey("user.name", String, false),
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: user.name
|
- name: user.name
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- String
|
- String
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Simple path promoted",
|
name: "Simple path promoted",
|
||||||
key: makeKey("user.name", String, true),
|
key: makeKey("user.name", String, true),
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: user.name
|
- name: user.name
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- String
|
- String
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
- name: user.name
|
- name: user.name
|
||||||
column: body_json_promoted
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- String
|
- String
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
maxDynamicPaths: 256
|
maxDynamicPaths: 256
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column, bodyPromotedColumn),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Empty path returns error",
|
name: "Empty path returns error",
|
||||||
@@ -278,8 +285,8 @@ func TestPlanJSON_BasicStructure(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
err := tt.key.SetJSONAccessPlan(JSONColumnMetadata{
|
err := tt.key.SetJSONAccessPlan(JSONColumnMetadata{
|
||||||
BaseColumn: "body_json",
|
BaseColumn: bodyV2Column,
|
||||||
PromotedColumn: "body_json_promoted",
|
PromotedColumn: bodyPromotedColumn,
|
||||||
}, types)
|
}, types)
|
||||||
if tt.expectErr {
|
if tt.expectErr {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -304,9 +311,9 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Single array level - JSON branch only",
|
name: "Single array level - JSON branch only",
|
||||||
path: "education[].name",
|
path: "education[].name",
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -318,14 +325,14 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
|
|||||||
maxDynamicTypes: 8
|
maxDynamicTypes: 8
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Single array level - both JSON and Dynamic branches",
|
name: "Single array level - both JSON and Dynamic branches",
|
||||||
path: "education[].awards[].type",
|
path: "education[].awards[].type",
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -352,14 +359,14 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
|
|||||||
maxDynamicPaths: 256
|
maxDynamicPaths: 256
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Deeply nested array path",
|
name: "Deeply nested array path",
|
||||||
path: "interests[].entities[].reviews[].entries[].metadata[].positions[].name",
|
path: "interests[].entities[].reviews[].entries[].metadata[].positions[].name",
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: interests
|
- name: interests
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -399,14 +406,14 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
|
|||||||
- String
|
- String
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ArrayAnyIndex replacement [*] to []",
|
name: "ArrayAnyIndex replacement [*] to []",
|
||||||
path: "education[*].name",
|
path: "education[*].name",
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -418,7 +425,7 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
|
|||||||
maxDynamicTypes: 8
|
maxDynamicTypes: 8
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,8 +433,8 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
key := makeKey(tt.path, String, false)
|
key := makeKey(tt.path, String, false)
|
||||||
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
||||||
BaseColumn: "body_json",
|
BaseColumn: bodyV2Column,
|
||||||
PromotedColumn: "body_json_promoted",
|
PromotedColumn: bodyPromotedColumn,
|
||||||
}, types)
|
}, types)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, key.JSONPlan)
|
require.NotNil(t, key.JSONPlan)
|
||||||
@@ -445,15 +452,15 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
|
|||||||
t.Run("Non-promoted plan", func(t *testing.T) {
|
t.Run("Non-promoted plan", func(t *testing.T) {
|
||||||
key := makeKey(path, String, false)
|
key := makeKey(path, String, false)
|
||||||
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
||||||
BaseColumn: "body_json",
|
BaseColumn: bodyV2Column,
|
||||||
PromotedColumn: "body_json_promoted",
|
PromotedColumn: bodyPromotedColumn,
|
||||||
}, types)
|
}, types)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, key.JSONPlan, 1)
|
require.Len(t, key.JSONPlan, 1)
|
||||||
|
|
||||||
expectedYAML := `
|
expectedYAML := fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -480,7 +487,7 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
|
|||||||
maxDynamicPaths: 256
|
maxDynamicPaths: 256
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`
|
`, bodyV2Column)
|
||||||
got := plansToYAML(t, key.JSONPlan)
|
got := plansToYAML(t, key.JSONPlan)
|
||||||
require.YAMLEq(t, expectedYAML, got)
|
require.YAMLEq(t, expectedYAML, got)
|
||||||
})
|
})
|
||||||
@@ -488,15 +495,15 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
|
|||||||
t.Run("Promoted plan", func(t *testing.T) {
|
t.Run("Promoted plan", func(t *testing.T) {
|
||||||
key := makeKey(path, String, true)
|
key := makeKey(path, String, true)
|
||||||
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
||||||
BaseColumn: "body_json",
|
BaseColumn: bodyV2Column,
|
||||||
PromotedColumn: "body_json_promoted",
|
PromotedColumn: bodyPromotedColumn,
|
||||||
}, types)
|
}, types)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, key.JSONPlan, 2)
|
require.Len(t, key.JSONPlan, 2)
|
||||||
|
|
||||||
expectedYAML := `
|
expectedYAML := fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -524,7 +531,7 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
|
|||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json_promoted
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -554,7 +561,7 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
|
|||||||
maxDynamicPaths: 256
|
maxDynamicPaths: 256
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`
|
`, bodyV2Column, bodyPromotedColumn)
|
||||||
got := plansToYAML(t, key.JSONPlan)
|
got := plansToYAML(t, key.JSONPlan)
|
||||||
require.YAMLEq(t, expectedYAML, got)
|
require.YAMLEq(t, expectedYAML, got)
|
||||||
})
|
})
|
||||||
@@ -575,11 +582,11 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
|
|||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Very deep nesting - validates progression doesn't go negative",
|
name: "Very deep nesting - validates progression doesn't go negative",
|
||||||
path: "interests[].entities[].reviews[].entries[].metadata[].positions[].name",
|
path: "interests[].entities[].reviews[].entries[].metadata[].positions[].name",
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: interests
|
- name: interests
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -619,14 +626,14 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
|
|||||||
- String
|
- String
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Path with mixed scalar and array types",
|
name: "Path with mixed scalar and array types",
|
||||||
path: "education[].type",
|
path: "education[].type",
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -639,20 +646,20 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
|
|||||||
maxDynamicTypes: 8
|
maxDynamicTypes: 8
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Exists with only array types available",
|
name: "Exists with only array types available",
|
||||||
path: "education",
|
path: "education",
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: Array(JSON)
|
elemType: Array(JSON)
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,8 +675,8 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
|
|||||||
}
|
}
|
||||||
key := makeKey(tt.path, keyType, false)
|
key := makeKey(tt.path, keyType, false)
|
||||||
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
||||||
BaseColumn: "body_json",
|
BaseColumn: bodyV2Column,
|
||||||
PromotedColumn: "body_json_promoted",
|
PromotedColumn: bodyPromotedColumn,
|
||||||
}, types)
|
}, types)
|
||||||
if tt.expectErr {
|
if tt.expectErr {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -687,15 +694,15 @@ func TestPlanJSON_TreeStructure(t *testing.T) {
|
|||||||
path := "education[].awards[].participated[].team[].branch"
|
path := "education[].awards[].participated[].team[].branch"
|
||||||
key := makeKey(path, String, false)
|
key := makeKey(path, String, false)
|
||||||
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
||||||
BaseColumn: "body_json",
|
BaseColumn: bodyV2Column,
|
||||||
PromotedColumn: "body_json_promoted",
|
PromotedColumn: bodyPromotedColumn,
|
||||||
}, types)
|
}, types)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, key.JSONPlan, 1)
|
require.Len(t, key.JSONPlan, 1)
|
||||||
|
|
||||||
expectedYAML := `
|
expectedYAML := fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -780,7 +787,7 @@ func TestPlanJSON_TreeStructure(t *testing.T) {
|
|||||||
maxDynamicPaths: 64
|
maxDynamicPaths: 64
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`
|
`, bodyV2Column)
|
||||||
|
|
||||||
got := plansToYAML(t, key.JSONPlan)
|
got := plansToYAML(t, key.JSONPlan)
|
||||||
require.YAMLEq(t, expectedYAML, got)
|
require.YAMLEq(t, expectedYAML, got)
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ type MockMetadataStore struct {
|
|||||||
PromotedPathsMap map[string]bool
|
PromotedPathsMap map[string]bool
|
||||||
LogsJSONIndexesMap map[string][]schemamigrator.Index
|
LogsJSONIndexesMap map[string][]schemamigrator.Index
|
||||||
LookupKeysMap map[telemetrytypes.MetricMetadataLookupKey]int64
|
LookupKeysMap map[telemetrytypes.MetricMetadataLookupKey]int64
|
||||||
|
// StaticFields holds signal-specific intrinsic field definitions (e.g. telemetrylogs.IntrinsicFields).
|
||||||
|
StaticFields map[string]telemetrytypes.TelemetryFieldKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMockMetadataStore creates a new instance of MockMetadataStore with initialized maps
|
// NewMockMetadataStore creates a new instance of MockMetadataStore with initialized maps.
|
||||||
func NewMockMetadataStore() *MockMetadataStore {
|
func NewMockMetadataStore() *MockMetadataStore {
|
||||||
return &MockMetadataStore{
|
return &MockMetadataStore{
|
||||||
KeysMap: make(map[string][]*telemetrytypes.TelemetryFieldKey),
|
KeysMap: make(map[string][]*telemetrytypes.TelemetryFieldKey),
|
||||||
@@ -33,12 +35,20 @@ func NewMockMetadataStore() *MockMetadataStore {
|
|||||||
PromotedPathsMap: make(map[string]bool),
|
PromotedPathsMap: make(map[string]bool),
|
||||||
LogsJSONIndexesMap: make(map[string][]schemamigrator.Index),
|
LogsJSONIndexesMap: make(map[string][]schemamigrator.Index),
|
||||||
LookupKeysMap: make(map[telemetrytypes.MetricMetadataLookupKey]int64),
|
LookupKeysMap: make(map[telemetrytypes.MetricMetadataLookupKey]int64),
|
||||||
|
StaticFields: make(map[string]telemetrytypes.TelemetryFieldKey),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetStaticFields sets the static fields for the mock metadata store.
|
||||||
|
// Pass the signal-specific intrinsic fields (e.g. telemetrylogs.IntrinsicFields) so the mock
|
||||||
|
// mirrors what the real metadata store does when injecting those definitions into key results.
|
||||||
|
func (m *MockMetadataStore) SetStaticFields(intrinsicFields map[string]telemetrytypes.TelemetryFieldKey) {
|
||||||
|
m.StaticFields = intrinsicFields
|
||||||
|
}
|
||||||
|
|
||||||
// GetKeys returns a map of field keys types.TelemetryFieldKey by name
|
// GetKeys returns a map of field keys types.TelemetryFieldKey by name
|
||||||
func (m *MockMetadataStore) GetKeys(ctx context.Context, fieldKeySelector *telemetrytypes.FieldKeySelector) (map[string][]*telemetrytypes.TelemetryFieldKey, bool, error) {
|
func (m *MockMetadataStore) GetKeys(ctx context.Context, fieldKeySelector *telemetrytypes.FieldKeySelector) (map[string][]*telemetrytypes.TelemetryFieldKey, bool, error) {
|
||||||
|
setOfKeys := make(map[string]*telemetrytypes.TelemetryFieldKey)
|
||||||
result := make(map[string][]*telemetrytypes.TelemetryFieldKey)
|
result := make(map[string][]*telemetrytypes.TelemetryFieldKey)
|
||||||
|
|
||||||
// If selector is nil, return all keys
|
// If selector is nil, return all keys
|
||||||
@@ -46,18 +56,30 @@ func (m *MockMetadataStore) GetKeys(ctx context.Context, fieldKeySelector *telem
|
|||||||
return m.KeysMap, true, nil
|
return m.KeysMap, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply selector logic
|
// Apply selector logic from KeysMap
|
||||||
for name, keys := range m.KeysMap {
|
for name, keys := range m.KeysMap {
|
||||||
// Check if name matches
|
|
||||||
if matchesName(fieldKeySelector, name) {
|
if matchesName(fieldKeySelector, name) {
|
||||||
filteredKeys := []*telemetrytypes.TelemetryFieldKey{}
|
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
if matchesKey(fieldKeySelector, key) {
|
if matchesKey(fieldKeySelector, key) {
|
||||||
filteredKeys = append(filteredKeys, key)
|
if _, exists := setOfKeys[key.Text()]; !exists {
|
||||||
|
result[name] = append(result[name], key)
|
||||||
|
setOfKeys[key.Text()] = key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(filteredKeys) > 0 {
|
}
|
||||||
result[name] = filteredKeys
|
}
|
||||||
|
|
||||||
|
// StaticFields (e.g. IntrinsicFields), mirroring the real metadata store.
|
||||||
|
for key, field := range m.StaticFields {
|
||||||
|
if !matchesName(fieldKeySelector, key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchesKey(fieldKeySelector, &field) {
|
||||||
|
if _, exists := setOfKeys[field.Text()]; !exists {
|
||||||
|
result[field.Name] = append(result[field.Name], &field)
|
||||||
|
setOfKeys[field.Text()] = &field
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +130,7 @@ func (m *MockMetadataStore) GetKey(ctx context.Context, fieldKeySelector *teleme
|
|||||||
|
|
||||||
result := []*telemetrytypes.TelemetryFieldKey{}
|
result := []*telemetrytypes.TelemetryFieldKey{}
|
||||||
|
|
||||||
// Find keys matching the selector
|
// Find keys matching the selector from KeysMap
|
||||||
for name, keys := range m.KeysMap {
|
for name, keys := range m.KeysMap {
|
||||||
if matchesName(fieldKeySelector, name) {
|
if matchesName(fieldKeySelector, name) {
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
@@ -119,6 +141,17 @@ func (m *MockMetadataStore) GetKey(ctx context.Context, fieldKeySelector *teleme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add matching StaticFields (e.g. IntrinsicFields), same as the real metadata store does
|
||||||
|
for key, field := range m.StaticFields {
|
||||||
|
if !matchesName(fieldKeySelector, key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchesKey(fieldKeySelector, &field) {
|
||||||
|
result = append(result, &field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,23 @@ def test_invite_and_register(
|
|||||||
assert invited_user["email"] == "editor@integration.test"
|
assert invited_user["email"] == "editor@integration.test"
|
||||||
assert invited_user["role"] == "EDITOR"
|
assert invited_user["role"] == "EDITOR"
|
||||||
|
|
||||||
|
# Verify the user user appears in the users list but as pending_invite status
|
||||||
|
response = requests.get(
|
||||||
|
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||||
|
timeout=2,
|
||||||
|
headers={"Authorization": f"Bearer {admin_token}"},
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTPStatus.OK
|
||||||
|
|
||||||
|
user_response = response.json()["data"]
|
||||||
|
found_user = next(
|
||||||
|
(user for user in user_response if user["email"] == "editor@integration.test"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
assert found_user is not None
|
||||||
|
assert found_user["status"] == "pending_invite"
|
||||||
|
assert found_user["role"] == "EDITOR"
|
||||||
|
|
||||||
reset_token = invited_user["token"]
|
reset_token = invited_user["token"]
|
||||||
|
|
||||||
# Reset the password to complete the invite flow (activates the user and also grants authz)
|
# Reset the password to complete the invite flow (activates the user and also grants authz)
|
||||||
@@ -231,85 +248,3 @@ def test_self_access(
|
|||||||
|
|
||||||
assert response.status_code == HTTPStatus.OK
|
assert response.status_code == HTTPStatus.OK
|
||||||
assert response.json()["data"]["role"] == "EDITOR"
|
assert response.json()["data"]["role"] == "EDITOR"
|
||||||
|
|
||||||
|
|
||||||
def test_old_invite_flow(signoz: types.SigNoz, get_token: Callable[[str, str], str]):
|
|
||||||
admin_token = get_token("admin@integration.test", "password123Z$")
|
|
||||||
|
|
||||||
# invite a new user
|
|
||||||
response = requests.post(
|
|
||||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
|
||||||
json={"email": "oldinviteflow@integration.test", "role": "VIEWER", "name": "old invite flow"},
|
|
||||||
timeout=2,
|
|
||||||
headers={"Authorization": f"Bearer {admin_token}"},
|
|
||||||
)
|
|
||||||
assert response.status_code == HTTPStatus.CREATED
|
|
||||||
|
|
||||||
# get the invite token using get api
|
|
||||||
response = requests.get(
|
|
||||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
|
||||||
timeout=2,
|
|
||||||
headers={
|
|
||||||
"Authorization": f"Bearer {admin_token}"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
invite_response = response.json()["data"]
|
|
||||||
found_invite = next(
|
|
||||||
(
|
|
||||||
invite
|
|
||||||
for invite in invite_response
|
|
||||||
if invite["email"] == "oldinviteflow@integration.test"
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
# accept the invite
|
|
||||||
response = requests.post(
|
|
||||||
signoz.self.host_configs["8080"].get("/api/v1/invite/accept"),
|
|
||||||
json={
|
|
||||||
"password": "password123Z$",
|
|
||||||
"displayName": "old invite flow",
|
|
||||||
"token": f"{found_invite['token']}",
|
|
||||||
},
|
|
||||||
timeout=2,
|
|
||||||
)
|
|
||||||
assert response.status_code == HTTPStatus.CREATED
|
|
||||||
|
|
||||||
# verify the invite token has been deleted
|
|
||||||
response = requests.get(
|
|
||||||
signoz.self.host_configs["8080"].get(f"/api/v1/invite/{found_invite['token']}"),
|
|
||||||
timeout=2,
|
|
||||||
)
|
|
||||||
assert response.status_code in (HTTPStatus.NOT_FOUND, HTTPStatus.BAD_REQUEST)
|
|
||||||
|
|
||||||
# verify that admin endpoints cannot be called
|
|
||||||
response = requests.get(
|
|
||||||
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
|
||||||
timeout=2,
|
|
||||||
headers={
|
|
||||||
"Authorization": f"Bearer {get_token("oldinviteflow@integration.test", "password123Z$")}"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert response.status_code == HTTPStatus.FORBIDDEN
|
|
||||||
|
|
||||||
# verify the user has been created
|
|
||||||
response = requests.get(
|
|
||||||
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
|
||||||
timeout=2,
|
|
||||||
headers={
|
|
||||||
"Authorization": f"Bearer {admin_token}"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert response.status_code == HTTPStatus.OK
|
|
||||||
|
|
||||||
user_response = response.json()["data"]
|
|
||||||
found_user = next(
|
|
||||||
(user for user in user_response if user["email"] == "oldinviteflow@integration.test"),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert found_user is not None
|
|
||||||
assert found_user["role"] == "VIEWER"
|
|
||||||
assert found_user["displayName"] == "old invite flow"
|
|
||||||
assert found_user["email"] == "oldinviteflow@integration.test"
|
|
||||||
|
|||||||
@@ -61,3 +61,55 @@ def test_api_key(signoz: types.SigNoz, get_token: Callable[[str, str], str]) ->
|
|||||||
assert found_pat["userId"] == found_user["id"]
|
assert found_pat["userId"] == found_user["id"]
|
||||||
assert found_pat["name"] == "admin"
|
assert found_pat["name"] == "admin"
|
||||||
assert found_pat["role"] == "ADMIN"
|
assert found_pat["role"] == "ADMIN"
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_key_role(signoz: types.SigNoz, get_token: Callable[[str, str], str]) -> None:
|
||||||
|
admin_token = get_token("admin@integration.test", "password123Z$")
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
signoz.self.host_configs["8080"].get("/api/v1/pats"),
|
||||||
|
headers={"Authorization": f"Bearer {admin_token}"},
|
||||||
|
json={
|
||||||
|
"name": "viewer",
|
||||||
|
"role": "VIEWER",
|
||||||
|
"expiresInDays": 1,
|
||||||
|
},
|
||||||
|
timeout=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == HTTPStatus.CREATED
|
||||||
|
pat_response = response.json()
|
||||||
|
assert "data" in pat_response
|
||||||
|
assert "token" in pat_response["data"]
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||||
|
timeout=2,
|
||||||
|
headers={"SIGNOZ-API-KEY": f"{pat_response["data"]["token"]}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == HTTPStatus.FORBIDDEN
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
signoz.self.host_configs["8080"].get("/api/v1/pats"),
|
||||||
|
headers={"Authorization": f"Bearer {admin_token}"},
|
||||||
|
json={
|
||||||
|
"name": "editor",
|
||||||
|
"role": "EDITOR",
|
||||||
|
"expiresInDays": 1,
|
||||||
|
},
|
||||||
|
timeout=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == HTTPStatus.CREATED
|
||||||
|
pat_response = response.json()
|
||||||
|
assert "data" in pat_response
|
||||||
|
assert "token" in pat_response["data"]
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||||
|
timeout=2,
|
||||||
|
headers={"SIGNOZ-API-KEY": f"{pat_response["data"]["token"]}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == HTTPStatus.FORBIDDEN
|
||||||
|
|||||||
Reference in New Issue
Block a user