Compare commits

...

18 Commits

Author SHA1 Message Date
SagarRajput-7
b9bcd2d4e1 feat: refactored the member status mapping 2026-03-18 23:42:53 +05:30
SagarRajput-7
34dcd79243 feat: updated the confirmation dialog description as now the we cant permanently delete the member 2026-03-18 21:06:43 +05:30
SagarRajput-7
145d6327a7 feat: feedback, refactor and test mock update 2026-03-18 20:29:11 +05:30
SagarRajput-7
61cfd33fc6 feat: test case and pagination fix 2026-03-18 20:29:11 +05:30
SagarRajput-7
b299d63263 feat: changed text for copy, cancel and ingeneral messaging for invited users 2026-03-18 20:29:11 +05:30
SagarRajput-7
94f3e6d6d7 feat: delete orphaned type files 2026-03-18 20:29:11 +05:30
SagarRajput-7
7ae0a23103 feat: removed deprecated invite endpoint apis 2026-03-18 20:29:11 +05:30
SagarRajput-7
2d91b5fd0b feat: updated members page with new status response and remove invite endpoint api 2026-03-18 20:29:11 +05:30
Karan Balani
ab1428d413 fix: allow pending user to be updated 2026-03-18 20:28:02 +05:30
Karan Balani
9c859e4d07 chore: add back validation for pending user in list user apis for integration tests 2026-03-18 20:25:39 +05:30
Karan Balani
d6de4d58f7 chore: deprecate old user invite apis 2026-03-18 20:25:39 +05:30
Vinicius Lourenço
e52c5683dd feat(signozhq-ui): add @signozhq/ui lib (#10616) 2026-03-18 13:44:25 +00:00
Abhi kumar
90e3cb6775 feat: replaced external apis barchart with the new bar chart (#10460)
* feat: replaced external apis barchart with the new bar chart

* fix: tests

* chore: fixed tsc
2026-03-18 13:36:23 +00:00
primus-bot[bot]
155f287462 chore(release): bump to v0.116.1 (#10635)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-03-18 12:28:33 +00:00
Piyush Singariya
c8fcc48022 Revert "fix: "In Progress" stuck agent config (#10476)" (#10633)
This reverts commit fd19ff8e5e.
2026-03-18 11:30:39 +00:00
Vikrant Gupta
44b6885639 fix(identn): identn provider claims (#10631)
* fix(identn): identn provider claims

* fix(identn): add integration tests

* fix(identn): use identn provider from claims
2026-03-18 11:23:50 +00:00
Piyush Singariya
0e5a128325 refactor: consolidate body column for JSON logs (#10325)
* feat: has JSON QB

* fix: tests expected queries and values

* fix: ignored .vscode in gitignore

* fix: tests GroupBy

* revert: gitignore change

* fix: build json plans in metadata

* fix: empty filteredArrays condition

* fix: tests

* fix: tests

* fix: json qb test fix

* fix: review based on tushar

* fix: changes based on review from Srikanth

* fix: remove unnecessary bool checking

* fix: removed comment

* fix: merge json body columns together

* chore: var renamed

* fix: merge conflict

* test: fix

* fix: tests

* fix: go test flakiness

* chore: merge json fields

* fix: handle datatype collision

* revert: few unrelated changes

* revert: more unrelated change

* test: blocked on pr #10153

* feat: mapping body_v2.message:string map to body

* fix: go.mod required changes

* fix: remove unused function

* fix: test fixed

* fix: go mod changes

* fix: tests

* fix: go lint

* revert: remvoing unused function

* revert: change ReadMultiple is needed

* fix: body.message not being mapped correctly

* fix: append warnings from fieldkeys

* fix: change warning to a const to fix tests

* chore: addressing comments from Nitya

* chore: remove unnecessary change

* fix: shift warning attachment to getKeySelectors

* fix: lint error

* feat: update message as typehint in JSON Column (#10545)

* fix: cursor comments

* chore: minor changes based on review

* fix: message field key search in JSON Logs (#10577)

* feat: work in progress

* fix: test run success

* fix: in progress

* fix: excluding message from metadata fetch

* test: cleared

* fix: key name in metadata

* fix: uncomment tests

* chore: change to method for staticfields

* fix: remove confusing comments; remove usage of logical keyword

* chore: shift method above business logic

* chore: changes based on review

* fix: comments in metadata_store.go

* fix: fallback expr switch case

* revert: remove unused JSON Field datatype

* fix: remove the exception checking

* chore: keep message contained to field mapper

* chore: text search tests

* fix: package test fixed

* fix: redundant code block removal

* fix: retain staticfield implementation and spell fix

* fix: nil param lint

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2026-03-18 10:48:17 +00:00
Piyush Singariya
fd19ff8e5e fix: "In Progress" stuck agent config (#10476)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix: in progress status stuck in logs pipelines

* fix: stuck in progress logs pipeline status

* fix: changes based on review

* revert: comment change

* fix: change order of handling updation

* fix: check newstatus deploy status
2026-03-18 08:31:26 +00:00
69 changed files with 1245 additions and 2235 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)'],

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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';

View File

@@ -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">

View File

@@ -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',

View File

@@ -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>
); );
} }

View File

@@ -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="" />);

View File

@@ -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 (

View File

@@ -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;
};

View File

@@ -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();
}); });

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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}
/> />
</> </>
); );

View File

@@ -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 });

View File

@@ -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;
}
} }

View File

@@ -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',
},
],
},
};

View File

@@ -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(

View File

@@ -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

View File

@@ -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();
});
});
});

View File

@@ -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;
}

View File

@@ -1,7 +0,0 @@
export interface Props {
id: string;
}
export interface PayloadProps {
data: string;
}

View File

@@ -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'];
}

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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
View File

@@ -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

View File

@@ -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"},

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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

View File

@@ -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)
} }

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}
}
}
}
}

View File

@@ -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.)
} }

View File

@@ -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")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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)

View File

@@ -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)
}
})
}
}

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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,
}, },
}, },

View File

@@ -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 }

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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
} }

View File

@@ -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"

View File

@@ -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