mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-09 06:30:37 +01:00
Compare commits
11 Commits
main
...
ns/saved-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40a036f2b0 | ||
|
|
d752452d40 | ||
|
|
4a8a1efb03 | ||
|
|
7d2afa5a54 | ||
|
|
b579190140 | ||
|
|
6222204af7 | ||
|
|
0fcd6aeada | ||
|
|
32af113d0c | ||
|
|
61588b9303 | ||
|
|
dbc40efe24 | ||
|
|
33a428496b |
@@ -7880,17 +7880,20 @@ components:
|
||||
type: string
|
||||
SavedviewtypesPostableSavedView:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
generateName:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
schemaVersion:
|
||||
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- source
|
||||
- data
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
SavedviewtypesSavedView:
|
||||
properties:
|
||||
@@ -7899,14 +7902,16 @@ components:
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
schemaVersion:
|
||||
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
@@ -7914,14 +7919,6 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
SavedviewtypesSavedViewData:
|
||||
properties:
|
||||
schemaVersion:
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
@@ -7936,6 +7933,7 @@ components:
|
||||
queries:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryEnvelope'
|
||||
minItems: 1
|
||||
type: array
|
||||
selectedFields:
|
||||
items:
|
||||
@@ -7945,9 +7943,11 @@ components:
|
||||
- displayName
|
||||
- panelType
|
||||
- queries
|
||||
- selectedFields
|
||||
- display
|
||||
type: object
|
||||
SavedviewtypesSchemaVersion:
|
||||
enum:
|
||||
- v2
|
||||
type: string
|
||||
SavedviewtypesSource:
|
||||
enum:
|
||||
- traces
|
||||
@@ -7957,13 +7957,16 @@ components:
|
||||
type: string
|
||||
SavedviewtypesUpdatableSavedView:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
schemaVersion:
|
||||
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- source
|
||||
- data
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
|
||||
properties:
|
||||
@@ -8938,6 +8941,15 @@ components:
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
TypesPostableBulkInviteRequest:
|
||||
properties:
|
||||
invites:
|
||||
items:
|
||||
$ref: '#/components/schemas/TypesPostableInvite'
|
||||
type: array
|
||||
required:
|
||||
- invites
|
||||
type: object
|
||||
TypesPostableForgotPassword:
|
||||
properties:
|
||||
email:
|
||||
@@ -11545,6 +11557,57 @@ paths:
|
||||
summary: Create invite
|
||||
tags:
|
||||
- users
|
||||
/api/v1/invite/bulk:
|
||||
post:
|
||||
deprecated: true
|
||||
description: This endpoint creates a bulk invite for a user
|
||||
operationId: CreateBulkInvite
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TypesPostableBulkInviteRequest'
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Create bulk invite
|
||||
tags:
|
||||
- users
|
||||
/api/v1/llm_pricing_rules:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -14468,6 +14531,177 @@ paths:
|
||||
summary: List users
|
||||
tags:
|
||||
- users
|
||||
/api/v1/user/{id}:
|
||||
delete:
|
||||
deprecated: true
|
||||
description: This endpoint deletes the user by id
|
||||
operationId: DeleteUserDeprecated
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"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 user
|
||||
tags:
|
||||
- users
|
||||
get:
|
||||
deprecated: true
|
||||
description: This endpoint returns the user by id
|
||||
operationId: GetUserDeprecated
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/TypesDeprecatedUser'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Get user
|
||||
tags:
|
||||
- users
|
||||
put:
|
||||
deprecated: true
|
||||
description: This endpoint updates the user by id
|
||||
operationId: UpdateUserDeprecated
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TypesDeprecatedUser'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/TypesDeprecatedUser'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"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: Update user
|
||||
tags:
|
||||
- users
|
||||
/api/v1/user/me:
|
||||
get:
|
||||
deprecated: true
|
||||
@@ -22741,6 +22975,12 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
|
||||
@@ -8991,8 +8991,17 @@ export enum SavedviewtypesPanelTypeDTO {
|
||||
list = 'list',
|
||||
trace = 'trace',
|
||||
}
|
||||
export enum SavedviewtypesSchemaVersionDTO {
|
||||
v2 = 'v2',
|
||||
}
|
||||
export enum SavedviewtypesSourceDTO {
|
||||
traces = 'traces',
|
||||
logs = 'logs',
|
||||
metrics = 'metrics',
|
||||
meter = 'meter',
|
||||
}
|
||||
export interface SavedviewtypesSavedViewSpecDTO {
|
||||
display: SavedviewtypesDisplayDTO;
|
||||
display?: SavedviewtypesDisplayDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -9005,25 +9014,10 @@ export interface SavedviewtypesSavedViewSpecDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
selectedFields: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
selectedFields?: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
}
|
||||
|
||||
export interface SavedviewtypesSavedViewDataDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
schemaVersion: string;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export enum SavedviewtypesSourceDTO {
|
||||
traces = 'traces',
|
||||
logs = 'logs',
|
||||
metrics = 'metrics',
|
||||
meter = 'meter',
|
||||
}
|
||||
export interface SavedviewtypesPostableSavedViewDTO {
|
||||
data: SavedviewtypesSavedViewDataDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -9032,7 +9026,9 @@ export interface SavedviewtypesPostableSavedViewDTO {
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO;
|
||||
source: SavedviewtypesSourceDTO;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export interface SavedviewtypesSavedViewDTO {
|
||||
@@ -9045,7 +9041,6 @@ export interface SavedviewtypesSavedViewDTO {
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
data?: SavedviewtypesSavedViewDataDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -9054,7 +9049,9 @@ export interface SavedviewtypesSavedViewDTO {
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO;
|
||||
source?: SavedviewtypesSourceDTO;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -9067,8 +9064,9 @@ export interface SavedviewtypesSavedViewDTO {
|
||||
}
|
||||
|
||||
export interface SavedviewtypesUpdatableSavedViewDTO {
|
||||
data: SavedviewtypesSavedViewDataDTO;
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO;
|
||||
source: SavedviewtypesSourceDTO;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {
|
||||
@@ -10057,21 +10055,6 @@ export interface TypesOrganizationDTO {
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface TypesPostableForgotPasswordDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
email: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
frontendBaseURL?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId: string;
|
||||
}
|
||||
|
||||
export interface TypesPostableInviteDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -10091,6 +10074,28 @@ export interface TypesPostableInviteDTO {
|
||||
role?: string;
|
||||
}
|
||||
|
||||
export interface TypesPostableBulkInviteRequestDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
invites: TypesPostableInviteDTO[];
|
||||
}
|
||||
|
||||
export interface TypesPostableForgotPasswordDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
email: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
frontendBaseURL?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId: string;
|
||||
}
|
||||
|
||||
export interface TypesPostableResetPasswordDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -11201,6 +11206,31 @@ export type ListUsersDeprecated200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteUserDeprecatedPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetUserDeprecatedPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetUserDeprecated200 = {
|
||||
data: TypesDeprecatedUserDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateUserDeprecatedPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type UpdateUserDeprecated200 = {
|
||||
data: TypesDeprecatedUserDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetMyUserDeprecated200 = {
|
||||
data: TypesDeprecatedUserDTO;
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
CreateResetPasswordTokenPathParameters,
|
||||
CreateUser201,
|
||||
CreateUserRole201,
|
||||
DeleteUserDeprecatedPathParameters,
|
||||
DeleteUserPathParameters,
|
||||
DeleteUserRolePathParameters,
|
||||
GetMyUser200,
|
||||
@@ -36,6 +37,8 @@ import type {
|
||||
GetRolesByUserID200,
|
||||
GetRolesByUserIDPathParameters,
|
||||
GetUser200,
|
||||
GetUserDeprecated200,
|
||||
GetUserDeprecatedPathParameters,
|
||||
GetUserPathParameters,
|
||||
GetUserRole200,
|
||||
GetUserRolePathParameters,
|
||||
@@ -47,12 +50,16 @@ import type {
|
||||
RenderErrorResponseDTO,
|
||||
SetRoleByUserIDPathParameters,
|
||||
TypesChangePasswordRequestDTO,
|
||||
TypesDeprecatedUserDTO,
|
||||
TypesPostableBulkInviteRequestDTO,
|
||||
TypesPostableForgotPasswordDTO,
|
||||
TypesPostableInviteDTO,
|
||||
TypesPostableResetPasswordDTO,
|
||||
TypesPostableRoleDTO,
|
||||
TypesPostableVerifyResetPasswordTokenDTO,
|
||||
TypesUpdatableUserDTO,
|
||||
UpdateUserDeprecated200,
|
||||
UpdateUserDeprecatedPathParameters,
|
||||
UpdateUserPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
@@ -253,6 +260,91 @@ export const useCreateInvite = <
|
||||
> => {
|
||||
return useMutation(getCreateInviteMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint creates a bulk invite for a user
|
||||
* @deprecated
|
||||
* @summary Create bulk invite
|
||||
*/
|
||||
export const createBulkInvite = (
|
||||
typesPostableBulkInviteRequestDTO?: BodyType<TypesPostableBulkInviteRequestDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/invite/bulk`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: typesPostableBulkInviteRequestDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateBulkInviteMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createBulkInvite>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createBulkInvite>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createBulkInvite'];
|
||||
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 createBulkInvite>>,
|
||||
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createBulkInvite(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateBulkInviteMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createBulkInvite>>
|
||||
>;
|
||||
export type CreateBulkInviteMutationBody =
|
||||
| BodyType<TypesPostableBulkInviteRequestDTO>
|
||||
| undefined;
|
||||
export type CreateBulkInviteMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Create bulk invite
|
||||
*/
|
||||
export const useCreateBulkInvite = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createBulkInvite>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createBulkInvite>>,
|
||||
TError,
|
||||
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateBulkInviteMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint resets the password by token
|
||||
* @summary Reset password
|
||||
@@ -423,6 +515,295 @@ export const invalidateListUsersDeprecated = async (
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint deletes the user by id
|
||||
* @deprecated
|
||||
* @summary Delete user
|
||||
*/
|
||||
export const deleteUserDeprecated = (
|
||||
{ id }: DeleteUserDeprecatedPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/user/${id}`,
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteUserDeprecatedMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteUserDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteUserDeprecatedPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteUserDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteUserDeprecatedPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteUserDeprecated'];
|
||||
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 deleteUserDeprecated>>,
|
||||
{ pathParams: DeleteUserDeprecatedPathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteUserDeprecated(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteUserDeprecatedMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteUserDeprecated>>
|
||||
>;
|
||||
|
||||
export type DeleteUserDeprecatedMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Delete user
|
||||
*/
|
||||
export const useDeleteUserDeprecated = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteUserDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteUserDeprecatedPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteUserDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteUserDeprecatedPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getDeleteUserDeprecatedMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint returns the user by id
|
||||
* @deprecated
|
||||
* @summary Get user
|
||||
*/
|
||||
export const getUserDeprecated = (
|
||||
{ id }: GetUserDeprecatedPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetUserDeprecated200>({
|
||||
url: `/api/v1/user/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetUserDeprecatedQueryKey = ({
|
||||
id,
|
||||
}: GetUserDeprecatedPathParameters) => {
|
||||
return [`/api/v1/user/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetUserDeprecatedQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getUserDeprecated>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetUserDeprecatedPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getUserDeprecated>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetUserDeprecatedQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getUserDeprecated>>
|
||||
> = ({ signal }) => getUserDeprecated({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getUserDeprecated>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetUserDeprecatedQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getUserDeprecated>>
|
||||
>;
|
||||
export type GetUserDeprecatedQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Get user
|
||||
*/
|
||||
|
||||
export function useGetUserDeprecated<
|
||||
TData = Awaited<ReturnType<typeof getUserDeprecated>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetUserDeprecatedPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getUserDeprecated>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetUserDeprecatedQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Get user
|
||||
*/
|
||||
export const invalidateGetUserDeprecated = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetUserDeprecatedPathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetUserDeprecatedQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint updates the user by id
|
||||
* @deprecated
|
||||
* @summary Update user
|
||||
*/
|
||||
export const updateUserDeprecated = (
|
||||
{ id }: UpdateUserDeprecatedPathParameters,
|
||||
typesDeprecatedUserDTO?: BodyType<TypesDeprecatedUserDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<UpdateUserDeprecated200>({
|
||||
url: `/api/v1/user/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: typesDeprecatedUserDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateUserDeprecatedMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateUserDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateUserDeprecatedPathParameters;
|
||||
data?: BodyType<TypesDeprecatedUserDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateUserDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateUserDeprecatedPathParameters;
|
||||
data?: BodyType<TypesDeprecatedUserDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateUserDeprecated'];
|
||||
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 updateUserDeprecated>>,
|
||||
{
|
||||
pathParams: UpdateUserDeprecatedPathParameters;
|
||||
data?: BodyType<TypesDeprecatedUserDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateUserDeprecated(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateUserDeprecatedMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateUserDeprecated>>
|
||||
>;
|
||||
export type UpdateUserDeprecatedMutationBody =
|
||||
| BodyType<TypesDeprecatedUserDTO>
|
||||
| undefined;
|
||||
export type UpdateUserDeprecatedMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Update user
|
||||
*/
|
||||
export const useUpdateUserDeprecated = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateUserDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateUserDeprecatedPathParameters;
|
||||
data?: BodyType<TypesDeprecatedUserDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateUserDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateUserDeprecatedPathParameters;
|
||||
data?: BodyType<TypesDeprecatedUserDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateUserDeprecatedMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint returns the user I belong to
|
||||
* @deprecated
|
||||
|
||||
28
frontend/src/api/v1/invite/bulk/create.ts
Normal file
28
frontend/src/api/v1/invite/bulk/create.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { UsersProps } from 'types/api/user/inviteUsers';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useCreateBulkInvite` hook (or `createBulkInvite` fetcher) from
|
||||
* `api/generated/services/users` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
const inviteUsers = async (
|
||||
users: UsersProps,
|
||||
): Promise<SuccessResponseV2<null>> => {
|
||||
try {
|
||||
const response = await axios.post(`/invite/bulk`, users);
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: null,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default inviteUsers;
|
||||
@@ -80,7 +80,7 @@ function InviteMembers({
|
||||
weight="semibold"
|
||||
className={styles.headerCellRole}
|
||||
>
|
||||
Roles
|
||||
Role
|
||||
</Typography.Text>
|
||||
<div className={styles.headerCellAction} />
|
||||
</div>
|
||||
@@ -108,10 +108,11 @@ function InviteMembers({
|
||||
|
||||
<div className={styles.cellRole}>
|
||||
<RolesSelect
|
||||
mode="multiple"
|
||||
value={row.roleIds}
|
||||
onChange={(roleIds): void => updateRole(row.id, roleIds)}
|
||||
placeholder="Select roles"
|
||||
mode="single"
|
||||
value={row.roleId || undefined}
|
||||
onChange={(roleId): void => updateRole(row.id, roleId)}
|
||||
placeholder="Select role"
|
||||
allowClear={false}
|
||||
id={`invite-role-${row.id}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -68,8 +68,8 @@ describe('InviteMembers - Edge Cases', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
await expect(
|
||||
@@ -100,8 +100,8 @@ describe('InviteMembers - Edge Cases', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
await expect(
|
||||
@@ -132,17 +132,17 @@ describe('InviteMembers - Edge Cases', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
await expect(
|
||||
screen.findByTestId('invite-api-error'),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
const viewerElements = screen.getAllByTitle('Viewer');
|
||||
const viewerElements = screen.getAllByText('Viewer');
|
||||
await user.click(viewerElements[0]);
|
||||
const editorOptions = await screen.findAllByTitle('Editor');
|
||||
const editorOptions = await screen.findAllByText('Editor');
|
||||
await user.click(editorOptions[editorOptions.length - 1]);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -189,8 +189,8 @@ describe('InviteMembers - Edge Cases', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
const submitBtn = screen.getByTestId('submit-btn');
|
||||
await user.click(submitBtn);
|
||||
@@ -226,8 +226,8 @@ describe('InviteMembers - Edge Cases', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], ' alice@signoz.io ');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
|
||||
@@ -32,14 +32,14 @@ describe('InviteMembers - Rendering', () => {
|
||||
render(<InviteMembers />);
|
||||
|
||||
expect(screen.getByText('Email address')).toBeInTheDocument();
|
||||
expect(screen.getByText('Roles')).toBeInTheDocument();
|
||||
expect(screen.getByText('Role')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides header when showHeader is false', () => {
|
||||
render(<InviteMembers showHeader={false} />);
|
||||
|
||||
expect(screen.queryByText('Email address')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Roles')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Role')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders add button by default', () => {
|
||||
@@ -89,7 +89,7 @@ describe('InviteMembers - Rendering', () => {
|
||||
it('renders role select for each row', () => {
|
||||
render(<InviteMembers initialRowCount={2} />);
|
||||
|
||||
const roleSelects = screen.getAllByText('Select roles');
|
||||
const roleSelects = screen.getAllByText('Select role');
|
||||
expect(roleSelects).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,8 +40,8 @@ describe('InviteMembers - Submission', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], 'alice@signoz.io');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -73,17 +73,17 @@ describe('InviteMembers - Submission', () => {
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
|
||||
await user.type(emailInputs[0], 'alice@signoz.io');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await user.type(emailInputs[1], 'bob@signoz.io');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
const editorOptions = await screen.findAllByTitle('Editor');
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
const editorOptions = await screen.findAllByText('Editor');
|
||||
await user.click(editorOptions[editorOptions.length - 1]);
|
||||
|
||||
await user.type(emailInputs[2], 'charlie@signoz.io');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
const adminOptions = await screen.findAllByTitle('Admin');
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
const adminOptions = await screen.findAllByText('Admin');
|
||||
await user.click(adminOptions[adminOptions.length - 1]);
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
@@ -125,8 +125,8 @@ describe('InviteMembers - Submission', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -154,8 +154,8 @@ describe('InviteMembers - Submission', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -218,12 +218,12 @@ describe('InviteMembers - Submission', () => {
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
|
||||
await user.type(emailInputs[0], 'alice@signoz.io');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await user.type(emailInputs[1], 'bob@signoz.io');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
const editorOptions = await screen.findAllByTitle('Editor');
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
const editorOptions = await screen.findAllByText('Editor');
|
||||
await user.click(editorOptions[editorOptions.length - 1]);
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
@@ -276,8 +276,8 @@ describe('InviteMembers - Submission', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -303,8 +303,8 @@ describe('InviteMembers - Submission', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ describe('InviteMembers - Validation', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], INVALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -60,8 +60,8 @@ describe('InviteMembers - Validation', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], INVALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -85,8 +85,8 @@ describe('InviteMembers - Validation', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], INVALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
await expect(
|
||||
@@ -149,8 +149,8 @@ describe('InviteMembers - Validation', () => {
|
||||
screen.findByText('Please select roles for team members'),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
@@ -204,8 +204,8 @@ describe('InviteMembers - Validation', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await user.click(screen.getAllByText('Select role')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
|
||||
export interface InviteMemberRow {
|
||||
id: string;
|
||||
email: string;
|
||||
roleIds: string[];
|
||||
roleId: string;
|
||||
}
|
||||
|
||||
export interface InviteResult {
|
||||
@@ -38,7 +38,7 @@ export interface UseInviteMembersReturn {
|
||||
addRow: () => void;
|
||||
removeRow: (id: string) => void;
|
||||
updateEmail: (id: string, email: string) => void;
|
||||
updateRole: (id: string, roleIds: string[]) => void;
|
||||
updateRole: (id: string, roleId: string | undefined) => void;
|
||||
reset: () => void;
|
||||
submit: () => Promise<InviteResult[]>;
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ import {
|
||||
const createEmptyRow = (): InviteMemberRow => ({
|
||||
id: uuid(),
|
||||
email: '',
|
||||
roleIds: [],
|
||||
roleId: '',
|
||||
});
|
||||
|
||||
const isRowTouched = (row: InviteMemberRow): boolean =>
|
||||
row.email.trim() !== '' || row.roleIds.length > 0;
|
||||
row.email.trim() !== '' || row.roleId !== '';
|
||||
|
||||
export function useInviteMembers(
|
||||
options: UseInviteMembersOptions = {},
|
||||
@@ -78,7 +78,7 @@ export function useInviteMembers(
|
||||
|
||||
touched.forEach((row) => {
|
||||
const emailValid = EMAIL_REGEX.test(row.email);
|
||||
const roleValid = row.roleIds.length > 0;
|
||||
const roleValid = row.roleId !== '';
|
||||
|
||||
if (!emailValid || !row.email) {
|
||||
isValid = false;
|
||||
@@ -139,12 +139,12 @@ export function useInviteMembers(
|
||||
);
|
||||
|
||||
const updateRole = useCallback(
|
||||
(id: string, roleIds: string[]): void => {
|
||||
(id: string, roleId: string | undefined): void => {
|
||||
setRows((prev) => {
|
||||
const updated = cloneDeep(prev);
|
||||
const row = updated.find((r) => r.id === id);
|
||||
if (row) {
|
||||
row.roleIds = roleIds;
|
||||
row.roleId = roleId ?? '';
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
@@ -187,7 +187,7 @@ export function useInviteMembers(
|
||||
await createUser({
|
||||
email: row.email.trim(),
|
||||
frontendBaseUrl: getBaseUrl(),
|
||||
userRoles: row.roleIds.map((id) => ({ id })),
|
||||
userRoles: [{ id: row.roleId }],
|
||||
});
|
||||
results.push({ email: row.email, success: true });
|
||||
} catch (err) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import './InviteTeamMembers.styles.scss';
|
||||
|
||||
interface TeamMember {
|
||||
email: string;
|
||||
roles: string[];
|
||||
role: string;
|
||||
name: string;
|
||||
frontendBaseUrl: string;
|
||||
id: string;
|
||||
@@ -45,7 +45,7 @@ function InviteTeamMembers({
|
||||
const toTeamMembers = (rows: InviteMemberRow[]): TeamMember[] =>
|
||||
rows.map((row) => ({
|
||||
email: row.email,
|
||||
roles: row.roleIds.map((roleId) => roleIdToName[roleId] ?? roleId),
|
||||
role: roleIdToName[row.roleId] ?? row.roleId,
|
||||
name: '',
|
||||
frontendBaseUrl: getBaseUrl(),
|
||||
id: row.id,
|
||||
|
||||
@@ -166,8 +166,8 @@ describe('InviteTeamMembers', () => {
|
||||
{ email: 'user2@test.com', success: true },
|
||||
];
|
||||
const mockRows: InviteMemberRow[] = [
|
||||
{ id: 'row-1', email: 'user1@test.com', roleIds: ['role-viewer-id'] },
|
||||
{ id: 'row-2', email: 'user2@test.com', roleIds: ['role-editor-id'] },
|
||||
{ id: 'row-1', email: 'user1@test.com', roleId: 'role-viewer-id' },
|
||||
{ id: 'row-2', email: 'user2@test.com', roleId: 'role-editor-id' },
|
||||
];
|
||||
mockInviteMembersProps?.onSuccess?.(mockResults, mockRows);
|
||||
|
||||
@@ -177,14 +177,14 @@ describe('InviteTeamMembers', () => {
|
||||
teamMembers: [
|
||||
{
|
||||
email: 'user1@test.com',
|
||||
roles: ['VIEWER'],
|
||||
role: 'VIEWER',
|
||||
name: '',
|
||||
frontendBaseUrl: 'http://localhost:3301',
|
||||
id: 'row-1',
|
||||
},
|
||||
{
|
||||
email: 'user2@test.com',
|
||||
roles: ['EDITOR'],
|
||||
role: 'EDITOR',
|
||||
name: '',
|
||||
frontendBaseUrl: 'http://localhost:3301',
|
||||
id: 'row-2',
|
||||
@@ -211,8 +211,8 @@ describe('InviteTeamMembers', () => {
|
||||
{ email: 'user2@test.com', success: false, error: 'Already exists' },
|
||||
];
|
||||
const mockRows: InviteMemberRow[] = [
|
||||
{ id: 'row-1', email: 'user1@test.com', roleIds: ['role-viewer-id'] },
|
||||
{ id: 'row-2', email: 'user2@test.com', roleIds: ['role-admin-id'] },
|
||||
{ id: 'row-1', email: 'user1@test.com', roleId: 'role-viewer-id' },
|
||||
{ id: 'row-2', email: 'user2@test.com', roleId: 'role-admin-id' },
|
||||
];
|
||||
mockInviteMembersProps?.onPartialSuccess?.(mockResults, mockRows);
|
||||
|
||||
@@ -222,14 +222,14 @@ describe('InviteTeamMembers', () => {
|
||||
teamMembers: [
|
||||
{
|
||||
email: 'user1@test.com',
|
||||
roles: ['VIEWER'],
|
||||
role: 'VIEWER',
|
||||
name: '',
|
||||
frontendBaseUrl: 'http://localhost:3301',
|
||||
id: 'row-1',
|
||||
},
|
||||
{
|
||||
email: 'user2@test.com',
|
||||
roles: ['ADMIN'],
|
||||
role: 'ADMIN',
|
||||
name: '',
|
||||
frontendBaseUrl: 'http://localhost:3301',
|
||||
id: 'row-2',
|
||||
@@ -252,8 +252,8 @@ describe('InviteTeamMembers', () => {
|
||||
{ email: 'user2@test.com', success: false, error: 'Error 2' },
|
||||
];
|
||||
const mockRows: InviteMemberRow[] = [
|
||||
{ id: 'row-1', email: 'user1@test.com', roleIds: ['role-editor-id'] },
|
||||
{ id: 'row-2', email: 'user2@test.com', roleIds: ['role-viewer-id'] },
|
||||
{ id: 'row-1', email: 'user1@test.com', roleId: 'role-editor-id' },
|
||||
{ id: 'row-2', email: 'user2@test.com', roleId: 'role-viewer-id' },
|
||||
];
|
||||
mockInviteMembersProps?.onAllFailed?.(mockResults, mockRows);
|
||||
|
||||
@@ -263,14 +263,14 @@ describe('InviteTeamMembers', () => {
|
||||
teamMembers: [
|
||||
{
|
||||
email: 'user1@test.com',
|
||||
roles: ['EDITOR'],
|
||||
role: 'EDITOR',
|
||||
name: '',
|
||||
frontendBaseUrl: 'http://localhost:3301',
|
||||
id: 'row-1',
|
||||
},
|
||||
{
|
||||
email: 'user2@test.com',
|
||||
roles: ['VIEWER'],
|
||||
role: 'VIEWER',
|
||||
name: '',
|
||||
frontendBaseUrl: 'http://localhost:3301',
|
||||
id: 'row-2',
|
||||
|
||||
@@ -194,6 +194,14 @@ export const handlers = [
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.put('http://localhost/api/v1/user/:id', (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: 'user updated successfully',
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'http://localhost/api/v3/autocomplete/aggregate_attributes',
|
||||
(req, res, ctx) =>
|
||||
|
||||
12
frontend/src/types/api/user/inviteUsers.ts
Normal file
12
frontend/src/types/api/user/inviteUsers.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { User } from 'types/reducer/app';
|
||||
|
||||
export interface UserProps {
|
||||
name: User['displayName'];
|
||||
email: User['email'];
|
||||
role: string;
|
||||
frontendBaseUrl: string;
|
||||
}
|
||||
|
||||
export interface UsersProps {
|
||||
invites: UserProps[];
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func (provider *provider) addSavedViewRoutes(router *mux.Router) error {
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceSavedView.Scope(coretypes.VerbCreate)}),
|
||||
},
|
||||
|
||||
@@ -27,6 +27,22 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/invite/bulk", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.CreateBulkInvite), handler.OpenAPIDef{
|
||||
ID: "CreateBulkInvite",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Create bulk invite",
|
||||
Description: "This endpoint creates a bulk invite for a user",
|
||||
Request: new(types.PostableBulkInviteRequest),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: true,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/user", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.ListUsersDeprecated), handler.OpenAPIDef{
|
||||
ID: "ListUsersDeprecated",
|
||||
Tags: []string{"users"},
|
||||
@@ -129,6 +145,23 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authzMiddleware.SelfAccess(provider.userHandler.GetUserDeprecated), handler.OpenAPIDef{
|
||||
ID: "GetUserDeprecated",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Get user",
|
||||
Description: "This endpoint returns the user by id",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(types.DeprecatedUser),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: true,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.GetUser), handler.OpenAPIDef{
|
||||
ID: "GetUser",
|
||||
Tags: []string{"users"},
|
||||
@@ -146,6 +179,23 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authzMiddleware.SelfAccess(provider.userHandler.UpdateUserDeprecated), handler.OpenAPIDef{
|
||||
ID: "UpdateUserDeprecated",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Update user",
|
||||
Description: "This endpoint updates the user by id",
|
||||
Request: new(types.DeprecatedUser),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(types.DeprecatedUser),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: true,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.UpdateUser), handler.OpenAPIDef{
|
||||
ID: "UpdateUser",
|
||||
Tags: []string{"users"},
|
||||
@@ -163,6 +213,23 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.DeleteUser), handler.OpenAPIDef{
|
||||
ID: "DeleteUserDeprecated",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Delete user",
|
||||
Description: "This endpoint deletes the user by id",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: true,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.DeleteUser), handler.OpenAPIDef{
|
||||
ID: "DeleteUser",
|
||||
Tags: []string{"users"},
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -150,6 +151,40 @@ func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc {
|
||||
})
|
||||
}
|
||||
|
||||
func (middleware *AuthZ) SelfAccess(next http.HandlerFunc) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(req.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
selectors := []coretypes.Selector{
|
||||
coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName),
|
||||
}
|
||||
|
||||
err = middleware.authzService.CheckWithTupleCreation(
|
||||
req.Context(),
|
||||
claims,
|
||||
valuer.MustNewUUID(claims.OrgID),
|
||||
authtypes.Relation{Verb: coretypes.VerbAssignee},
|
||||
coretypes.NewResourceRole(),
|
||||
selectors,
|
||||
selectors,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
id := mux.Vars(req)["id"]
|
||||
if err := claims.IsSelfAccess(id); err != nil {
|
||||
middleware.logger.WarnContext(req.Context(), authzDeniedMessage, slog.Any("claims", claims))
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
next(rw, req)
|
||||
})
|
||||
}
|
||||
|
||||
func (middleware *AuthZ) OpenAccess(next http.HandlerFunc) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
next(rw, req)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": false
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": false
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": false
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": false
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": false
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
|
||||
@@ -36,70 +36,73 @@ type legacyExtraData struct {
|
||||
}
|
||||
|
||||
// newPostableSavedViewFromLegacyView builds a create payload for a v1 request.
|
||||
func newPostableSavedViewFromLegacyView(v *v3.SavedView) savedviewtypes.PostableSavedView {
|
||||
// Unlike migration 109 (which processes historical rows it can't ask anyone
|
||||
// about), this is a live request with a caller on the other end -- malformed
|
||||
// extraData is rejected outright rather than silently written as a partial row.
|
||||
func newPostableSavedViewFromLegacyView(v *v3.SavedView) (savedviewtypes.PostableSavedView, error) {
|
||||
var legacy legacyExtraData
|
||||
if v.ExtraData != "" {
|
||||
// Best-effort: malformed/older extraData shapes never fail the request
|
||||
_ = json.Unmarshal([]byte(v.ExtraData), &legacy)
|
||||
if err := json.Unmarshal([]byte(v.ExtraData), &legacy); err != nil {
|
||||
return savedviewtypes.PostableSavedView{}, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse extraData")
|
||||
}
|
||||
}
|
||||
|
||||
return savedviewtypes.PostableSavedView{
|
||||
GenerateName: true,
|
||||
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
|
||||
Data: savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: v.Name,
|
||||
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
|
||||
Queries: v.CompositeQuery.Queries,
|
||||
SelectedFields: legacy.SelectColumns,
|
||||
Display: savedviewtypes.Display{
|
||||
MaxLines: legacy.MaxLines,
|
||||
FontSize: legacy.FontSize,
|
||||
Format: legacy.Format,
|
||||
Color: legacy.Color,
|
||||
},
|
||||
GenerateName: true,
|
||||
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: v.Name,
|
||||
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
|
||||
Queries: v.CompositeQuery.Queries,
|
||||
SelectedFields: legacy.SelectColumns,
|
||||
Display: savedviewtypes.Display{
|
||||
MaxLines: legacy.MaxLines,
|
||||
FontSize: legacy.FontSize,
|
||||
Format: legacy.Format,
|
||||
Color: legacy.Color,
|
||||
},
|
||||
},
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newUpdatableSavedViewFromLegacyView builds an update payload for a v1 request.
|
||||
func newUpdatableSavedViewFromLegacyView(v *v3.SavedView) savedviewtypes.UpdatableSavedView {
|
||||
// See newPostableSavedViewFromLegacyView -- same reasoning for rejecting rather
|
||||
// than swallowing a malformed extraData.
|
||||
func newUpdatableSavedViewFromLegacyView(v *v3.SavedView) (savedviewtypes.UpdatableSavedView, error) {
|
||||
var legacy legacyExtraData
|
||||
if v.ExtraData != "" {
|
||||
// Best-effort: malformed/older extraData shapes never fail the request
|
||||
_ = json.Unmarshal([]byte(v.ExtraData), &legacy)
|
||||
if err := json.Unmarshal([]byte(v.ExtraData), &legacy); err != nil {
|
||||
return savedviewtypes.UpdatableSavedView{}, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse extraData")
|
||||
}
|
||||
}
|
||||
|
||||
return savedviewtypes.UpdatableSavedView{
|
||||
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
|
||||
Data: savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: v.Name,
|
||||
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
|
||||
Queries: v.CompositeQuery.Queries,
|
||||
SelectedFields: legacy.SelectColumns,
|
||||
Display: savedviewtypes.Display{
|
||||
MaxLines: legacy.MaxLines,
|
||||
FontSize: legacy.FontSize,
|
||||
Format: legacy.Format,
|
||||
Color: legacy.Color,
|
||||
},
|
||||
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: v.Name,
|
||||
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
|
||||
Queries: v.CompositeQuery.Queries,
|
||||
SelectedFields: legacy.SelectColumns,
|
||||
Display: savedviewtypes.Display{
|
||||
MaxLines: legacy.MaxLines,
|
||||
FontSize: legacy.FontSize,
|
||||
Format: legacy.Format,
|
||||
Color: legacy.Color,
|
||||
},
|
||||
},
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newLegacyViewFromSavedView renders a v2 SavedView back into the v1 shape.
|
||||
func newLegacyViewFromSavedView(v *savedviewtypes.SavedView) (*v3.SavedView, error) {
|
||||
extraData, err := json.Marshal(legacyExtraData{
|
||||
Color: v.Data.Spec.Display.Color,
|
||||
SelectColumns: v.Data.Spec.SelectedFields,
|
||||
Format: v.Data.Spec.Display.Format,
|
||||
MaxLines: v.Data.Spec.Display.MaxLines,
|
||||
FontSize: v.Data.Spec.Display.FontSize,
|
||||
Color: v.Spec.Display.Color,
|
||||
SelectColumns: v.Spec.SelectedFields,
|
||||
Format: v.Spec.Display.Format,
|
||||
MaxLines: v.Spec.Display.MaxLines,
|
||||
FontSize: v.Spec.Display.FontSize,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in marshalling extra data")
|
||||
@@ -107,17 +110,17 @@ func newLegacyViewFromSavedView(v *savedviewtypes.SavedView) (*v3.SavedView, err
|
||||
|
||||
return &v3.SavedView{
|
||||
ID: v.ID,
|
||||
Name: v.Data.Spec.DisplayName,
|
||||
Name: v.Spec.DisplayName,
|
||||
CreatedAt: v.CreatedAt,
|
||||
CreatedBy: v.CreatedBy,
|
||||
UpdatedAt: v.UpdatedAt,
|
||||
UpdatedBy: v.UpdatedBy,
|
||||
SourcePage: v.Source.StringValue(),
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
PanelType: v3.PanelType(v.Data.Spec.PanelType.StringValue()),
|
||||
PanelType: v3.PanelType(v.Spec.PanelType.StringValue()),
|
||||
// Saved views are only ever created from the explorer's builder mode.
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
Queries: v.Data.Spec.Queries,
|
||||
Queries: v.Spec.Queries,
|
||||
},
|
||||
ExtraData: string(extraData),
|
||||
}, nil
|
||||
@@ -156,7 +159,18 @@ func (handler *handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
uuid, err := handler.module.CreateView(ctx, claims.OrgID, newPostableSavedViewFromLegacyView(&view))
|
||||
postable, err := newPostableSavedViewFromLegacyView(&view)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := postable.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
uuid, err := handler.module.CreateView(ctx, claims.OrgID, postable)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
@@ -224,12 +238,22 @@ func (handler *handler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.module.UpdateView(ctx, claims.OrgID, viewUUID, newUpdatableSavedViewFromLegacyView(&view))
|
||||
updatable, err := newUpdatableSavedViewFromLegacyView(&view)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := updatable.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := handler.module.UpdateView(ctx, claims.OrgID, viewUUID, updatable); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, view)
|
||||
}
|
||||
|
||||
|
||||
@@ -38,17 +38,18 @@ func TestNewPostableSavedViewFromLegacyView(t *testing.T) {
|
||||
ExtraData: `{"color":"blue","selectColumns":[{"name":"service.name"}],"format":"table","maxLines":10,"fontSize":"large"}`,
|
||||
}
|
||||
|
||||
postable := newPostableSavedViewFromLegacyView(legacy)
|
||||
postable, err := newPostableSavedViewFromLegacyView(legacy)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, postable.Name, "v1 has no slug concept -- name must always be generated")
|
||||
assert.True(t, postable.GenerateName, "v1 has no slug concept -- name must always be generated")
|
||||
assert.Equal(t, "my view", postable.Data.Spec.DisplayName)
|
||||
assert.Equal(t, "my view", postable.Spec.DisplayName)
|
||||
assert.Equal(t, savedviewtypes.SourceLogs, postable.Source)
|
||||
assert.Equal(t, savedviewtypes.SavedViewSchemaVersion, postable.Data.SchemaVersion)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeGraph, postable.Data.Spec.PanelType)
|
||||
assert.Equal(t, legacy.CompositeQuery.Queries, postable.Data.Spec.Queries)
|
||||
assert.Equal(t, []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}}, postable.Data.Spec.SelectedFields)
|
||||
assert.Equal(t, savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"}, postable.Data.Spec.Display)
|
||||
assert.Equal(t, savedviewtypes.SavedViewSchemaVersion, postable.SchemaVersion)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeGraph, postable.Spec.PanelType)
|
||||
assert.Equal(t, legacy.CompositeQuery.Queries, postable.Spec.Queries)
|
||||
assert.Equal(t, []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}}, postable.Spec.SelectedFields)
|
||||
assert.Equal(t, savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"}, postable.Spec.Display)
|
||||
})
|
||||
|
||||
t.Run("empty extra data leaves display and selected fields zero-valued", func(t *testing.T) {
|
||||
@@ -62,13 +63,14 @@ func TestNewPostableSavedViewFromLegacyView(t *testing.T) {
|
||||
ExtraData: "",
|
||||
}
|
||||
|
||||
postable := newPostableSavedViewFromLegacyView(legacy)
|
||||
postable, err := newPostableSavedViewFromLegacyView(legacy)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, savedviewtypes.Display{}, postable.Data.Spec.Display)
|
||||
assert.Nil(t, postable.Data.Spec.SelectedFields)
|
||||
assert.Equal(t, savedviewtypes.Display{}, postable.Spec.Display)
|
||||
assert.Nil(t, postable.Spec.SelectedFields)
|
||||
})
|
||||
|
||||
t.Run("malformed extra data is ignored, not an error", func(t *testing.T) {
|
||||
t.Run("malformed extra data is rejected", func(t *testing.T) {
|
||||
legacy := &v3.SavedView{
|
||||
Name: "malformed extra data",
|
||||
SourcePage: "metrics",
|
||||
@@ -79,10 +81,26 @@ func TestNewPostableSavedViewFromLegacyView(t *testing.T) {
|
||||
ExtraData: `{not valid json`,
|
||||
}
|
||||
|
||||
postable := newPostableSavedViewFromLegacyView(legacy)
|
||||
_, err := newPostableSavedViewFromLegacyView(legacy)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
assert.Equal(t, "malformed extra data", postable.Data.Spec.DisplayName)
|
||||
assert.Equal(t, savedviewtypes.Display{}, postable.Data.Spec.Display)
|
||||
t.Run("legacy validation gap: empty builderQueries map with no queries", func(t *testing.T) {
|
||||
legacy := &v3.SavedView{
|
||||
Name: "no real queries",
|
||||
SourcePage: "logs",
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
PanelType: v3.PanelTypeGraph,
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, legacy.Validate(), "the legacy CompositeQuery check is expected to miss this")
|
||||
|
||||
postable, err := newPostableSavedViewFromLegacyView(legacy)
|
||||
require.NoError(t, err)
|
||||
assert.Error(t, postable.Validate(), "the converted postable must catch what the legacy check missed")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -97,26 +115,25 @@ func TestNewUpdatableSavedViewFromLegacyView(t *testing.T) {
|
||||
ExtraData: `{"color":"red"}`,
|
||||
}
|
||||
|
||||
updatable := newUpdatableSavedViewFromLegacyView(legacy)
|
||||
updatable, err := newUpdatableSavedViewFromLegacyView(legacy)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "renamed view", updatable.Data.Spec.DisplayName)
|
||||
assert.Equal(t, "renamed view", updatable.Spec.DisplayName)
|
||||
assert.Equal(t, savedviewtypes.SourceTraces, updatable.Source)
|
||||
}
|
||||
|
||||
func TestNewLegacyViewFromSavedView(t *testing.T) {
|
||||
now := time.Now()
|
||||
savedView := &savedviewtypes.SavedView{
|
||||
Name: "my-view-abc123ef",
|
||||
Source: savedviewtypes.SourceLogs,
|
||||
Data: savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: "my view",
|
||||
PanelType: savedviewtypes.PanelTypeGraph,
|
||||
Queries: testQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
|
||||
Display: savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"},
|
||||
},
|
||||
Name: "my-view-abc123ef",
|
||||
Source: savedviewtypes.SourceLogs,
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: "my view",
|
||||
PanelType: savedviewtypes.PanelTypeGraph,
|
||||
Queries: testQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
|
||||
Display: savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"},
|
||||
},
|
||||
}
|
||||
savedView.ID = valuer.GenerateUUID()
|
||||
@@ -129,7 +146,7 @@ func TestNewLegacyViewFromSavedView(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, savedView.ID, legacy.ID)
|
||||
assert.Equal(t, savedView.Data.Spec.DisplayName, legacy.Name)
|
||||
assert.Equal(t, savedView.Spec.DisplayName, legacy.Name)
|
||||
assert.Equal(t, savedView.CreatedAt, legacy.CreatedAt)
|
||||
assert.Equal(t, savedView.CreatedBy, legacy.CreatedBy)
|
||||
assert.Equal(t, savedView.UpdatedAt, legacy.UpdatedAt)
|
||||
@@ -137,20 +154,20 @@ func TestNewLegacyViewFromSavedView(t *testing.T) {
|
||||
assert.Equal(t, "logs", legacy.SourcePage)
|
||||
assert.Equal(t, v3.PanelTypeGraph, legacy.CompositeQuery.PanelType)
|
||||
assert.Equal(t, v3.QueryTypeBuilder, legacy.CompositeQuery.QueryType)
|
||||
assert.Equal(t, savedView.Data.Spec.Queries, legacy.CompositeQuery.Queries)
|
||||
assert.Equal(t, savedView.Spec.Queries, legacy.CompositeQuery.Queries)
|
||||
|
||||
var extra legacyExtraData
|
||||
require.NoError(t, json.Unmarshal([]byte(legacy.ExtraData), &extra))
|
||||
assert.Equal(t, "blue", extra.Color)
|
||||
assert.Equal(t, savedView.Data.Spec.SelectedFields, extra.SelectColumns)
|
||||
assert.Equal(t, savedView.Spec.SelectedFields, extra.SelectColumns)
|
||||
assert.Equal(t, "table", extra.Format)
|
||||
assert.Equal(t, 10, extra.MaxLines)
|
||||
assert.Equal(t, "large", extra.FontSize)
|
||||
}
|
||||
|
||||
func TestNewLegacyViewsFromSavedViews(t *testing.T) {
|
||||
a := &savedviewtypes.SavedView{Name: "a-slug", Source: savedviewtypes.SourceLogs, Data: savedviewtypes.SavedViewData{Spec: savedviewtypes.SavedViewSpec{DisplayName: "a", PanelType: savedviewtypes.PanelTypeGraph, Queries: testQueries()}}}
|
||||
b := &savedviewtypes.SavedView{Name: "b-slug", Source: savedviewtypes.SourceTraces, Data: savedviewtypes.SavedViewData{Spec: savedviewtypes.SavedViewSpec{DisplayName: "b", PanelType: savedviewtypes.PanelTypeTable, Queries: testQueries()}}}
|
||||
a := &savedviewtypes.SavedView{Name: "a-slug", Source: savedviewtypes.SourceLogs, Spec: savedviewtypes.SavedViewSpec{DisplayName: "a", PanelType: savedviewtypes.PanelTypeGraph, Queries: testQueries()}}
|
||||
b := &savedviewtypes.SavedView{Name: "b-slug", Source: savedviewtypes.SourceTraces, Spec: savedviewtypes.SavedViewSpec{DisplayName: "b", PanelType: savedviewtypes.PanelTypeTable, Queries: testQueries()}}
|
||||
|
||||
legacyViews, err := newLegacyViewsFromSavedViews([]*savedviewtypes.SavedView{a, b})
|
||||
require.NoError(t, err)
|
||||
@@ -167,31 +184,58 @@ func TestNewLegacyViewsFromSavedViews(t *testing.T) {
|
||||
// slug (Name) is deliberately NOT part of this contract -- v1 never sees it.
|
||||
func TestLegacyViewRoundTrip(t *testing.T) {
|
||||
original := &savedviewtypes.SavedView{
|
||||
Name: "round-trip-abc123ef",
|
||||
Source: savedviewtypes.SourceMetrics,
|
||||
Data: savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: "round trip",
|
||||
PanelType: savedviewtypes.PanelTypeTable,
|
||||
Queries: testQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
|
||||
Display: savedviewtypes.Display{MaxLines: 5, FontSize: "small", Format: "list", Color: "red"},
|
||||
},
|
||||
Name: "round-trip-abc123ef",
|
||||
Source: savedviewtypes.SourceMetrics,
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: "round trip",
|
||||
PanelType: savedviewtypes.PanelTypeTable,
|
||||
Queries: testQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
|
||||
Display: savedviewtypes.Display{MaxLines: 5, FontSize: "small", Format: "list", Color: "red"},
|
||||
},
|
||||
}
|
||||
|
||||
legacy, err := newLegacyViewFromSavedView(original)
|
||||
require.NoError(t, err)
|
||||
|
||||
roundTripped := newPostableSavedViewFromLegacyView(legacy)
|
||||
roundTripped, err := newPostableSavedViewFromLegacyView(legacy)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, roundTripped.Name)
|
||||
assert.True(t, roundTripped.GenerateName)
|
||||
assert.Equal(t, original.Data.Spec.DisplayName, roundTripped.Data.Spec.DisplayName)
|
||||
assert.Equal(t, original.Spec.DisplayName, roundTripped.Spec.DisplayName)
|
||||
assert.Equal(t, original.Source, roundTripped.Source)
|
||||
assert.Equal(t, original.Data.Spec.PanelType, roundTripped.Data.Spec.PanelType)
|
||||
assert.Equal(t, original.Data.Spec.Queries, roundTripped.Data.Spec.Queries)
|
||||
assert.Equal(t, original.Data.Spec.SelectedFields, roundTripped.Data.Spec.SelectedFields)
|
||||
assert.Equal(t, original.Data.Spec.Display, roundTripped.Data.Spec.Display)
|
||||
assert.Equal(t, original.Spec.PanelType, roundTripped.Spec.PanelType)
|
||||
assert.Equal(t, original.Spec.Queries, roundTripped.Spec.Queries)
|
||||
assert.Equal(t, original.Spec.SelectedFields, roundTripped.Spec.SelectedFields)
|
||||
assert.Equal(t, original.Spec.Display, roundTripped.Spec.Display)
|
||||
}
|
||||
|
||||
func TestLegacyViewRoundTrip_EmptySelectedFieldsAndDisplay(t *testing.T) {
|
||||
original := &savedviewtypes.SavedView{
|
||||
Name: "round-trip-empty-abc123ef",
|
||||
Source: savedviewtypes.SourceMetrics,
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: "round trip empty",
|
||||
PanelType: savedviewtypes.PanelTypeTable,
|
||||
Queries: testQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
|
||||
Display: savedviewtypes.Display{},
|
||||
},
|
||||
}
|
||||
|
||||
legacy, err := newLegacyViewFromSavedView(original)
|
||||
require.NoError(t, err)
|
||||
|
||||
var extra legacyExtraData
|
||||
require.NoError(t, json.Unmarshal([]byte(legacy.ExtraData), &extra))
|
||||
assert.Nil(t, extra.SelectColumns, "omitempty drops an empty selectColumns from extraData entirely")
|
||||
|
||||
roundTripped, err := newPostableSavedViewFromLegacyView(legacy)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, roundTripped.Spec.SelectedFields, "empty, not necessarily non-nil, on this leg of the round trip")
|
||||
assert.Equal(t, savedviewtypes.Display{}, roundTripped.Spec.Display)
|
||||
}
|
||||
|
||||
@@ -28,24 +28,22 @@ func newTestStore() (savedview.Module, *savedviewtypestest.StoreTest) {
|
||||
|
||||
func testPostableSavedView(name string, source savedviewtypes.Source) savedviewtypes.PostableSavedView {
|
||||
return savedviewtypes.PostableSavedView{
|
||||
Name: name,
|
||||
Source: source,
|
||||
Data: savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: name,
|
||||
PanelType: savedviewtypes.PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Aggregations: []qbtypes.LogAggregation{{Expression: "count()"}},
|
||||
},
|
||||
Name: name,
|
||||
Source: source,
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: name,
|
||||
PanelType: savedviewtypes.PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Aggregations: []qbtypes.LogAggregation{{Expression: "count()"}},
|
||||
},
|
||||
},
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
|
||||
},
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -53,8 +51,9 @@ func testPostableSavedView(name string, source savedviewtypes.Source) savedviewt
|
||||
func testUpdatableSavedView(displayName string, source savedviewtypes.Source) savedviewtypes.UpdatableSavedView {
|
||||
postable := testPostableSavedView(displayName, source)
|
||||
return savedviewtypes.UpdatableSavedView{
|
||||
Source: postable.Source,
|
||||
Data: postable.Data,
|
||||
Source: postable.Source,
|
||||
SchemaVersion: postable.SchemaVersion,
|
||||
Spec: postable.Spec,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +92,7 @@ func TestModule_CreateAndGetView(t *testing.T) {
|
||||
assert.Equal(t, savedviewtypes.SourceLogs, got.Source)
|
||||
assert.Equal(t, "creator@signoz.io", got.CreatedBy)
|
||||
assert.Equal(t, "creator@signoz.io", got.UpdatedBy)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeGraph, got.Data.Spec.PanelType)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeGraph, got.Spec.PanelType)
|
||||
|
||||
require.NoError(t, st.AssertExpectations())
|
||||
}
|
||||
@@ -138,21 +137,21 @@ func TestModule_UpdateView(t *testing.T) {
|
||||
existingName := existing.Name
|
||||
|
||||
updated := testUpdatableSavedView("renamed", savedviewtypes.SourceTraces)
|
||||
updated.Data.Spec.PanelType = savedviewtypes.PanelTypeTable
|
||||
updated.Spec.PanelType = savedviewtypes.PanelTypeTable
|
||||
|
||||
st.ExpectUpdate(orgID, id, 1)
|
||||
require.NoError(t, m.UpdateView(contextWithClaims(orgID, "updater@signoz.io"), orgID, id, updated))
|
||||
|
||||
stored := testSavedView(orgID, id, "updater@signoz.io", testPostableSavedView("renamed", savedviewtypes.SourceTraces))
|
||||
stored.Name = existingName
|
||||
stored.Data.Spec.PanelType = savedviewtypes.PanelTypeTable
|
||||
stored.Spec.PanelType = savedviewtypes.PanelTypeTable
|
||||
st.ExpectGet(orgID, id, stored)
|
||||
got, err := m.GetView(contextWithClaims(orgID, "creator@signoz.io"), orgID, id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingName, got.Name, "name must not change on update")
|
||||
assert.Equal(t, "renamed", got.Data.Spec.DisplayName)
|
||||
assert.Equal(t, "renamed", got.Spec.DisplayName)
|
||||
assert.Equal(t, savedviewtypes.SourceTraces, got.Source)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeTable, got.Data.Spec.PanelType)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeTable, got.Spec.PanelType)
|
||||
assert.Equal(t, "updater@signoz.io", got.UpdatedBy)
|
||||
|
||||
require.NoError(t, st.AssertExpectations())
|
||||
|
||||
@@ -19,7 +19,8 @@ func NewStore(sqlstore sqlstore.SQLStore) savedviewtypes.Store {
|
||||
}
|
||||
|
||||
func (store *store) Create(ctx context.Context, view *savedviewtypes.SavedView) error {
|
||||
_, err := store.sqlstore.BunDB().NewInsert().Model(view).Exec(ctx)
|
||||
storable := savedviewtypes.NewStorableSavedView(view)
|
||||
_, err := store.sqlstore.BunDB().NewInsert().Model(storable).Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "saved view with name %s already exists", view.Name)
|
||||
}
|
||||
@@ -27,23 +28,25 @@ func (store *store) Create(ctx context.Context, view *savedviewtypes.SavedView)
|
||||
}
|
||||
|
||||
func (store *store) Get(ctx context.Context, orgID string, id valuer.UUID) (*savedviewtypes.SavedView, error) {
|
||||
var view savedviewtypes.SavedView
|
||||
err := store.sqlstore.BunDB().NewSelect().Model(&view).Where("org_id = ? AND id = ?", orgID, id.StringValue()).Scan(ctx)
|
||||
var storable savedviewtypes.StorableSavedView
|
||||
err := store.sqlstore.BunDB().NewSelect().Model(&storable).Where("org_id = ? AND id = ?", orgID, id.StringValue()).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, savedviewtypes.ErrCodeSavedViewNotFound, "saved view %s not found", id.StringValue())
|
||||
}
|
||||
|
||||
normalizeSelectedFields(&view)
|
||||
return &view, nil
|
||||
view := storable.ToSavedView()
|
||||
normalizeSelectedFields(view)
|
||||
return view, nil
|
||||
}
|
||||
|
||||
func (store *store) Update(ctx context.Context, view *savedviewtypes.SavedView) error {
|
||||
storable := savedviewtypes.NewStorableSavedView(view)
|
||||
res, err := store.sqlstore.BunDB().NewUpdate().
|
||||
Model(&savedviewtypes.SavedView{}).
|
||||
Model((*savedviewtypes.StorableSavedView)(nil)).
|
||||
Set("updated_at = ?, updated_by = ?, source = ?, data = ?",
|
||||
view.UpdatedAt, view.UpdatedBy, view.Source, view.Data).
|
||||
Where("id = ?", view.ID.StringValue()).
|
||||
Where("org_id = ?", view.OrgID).
|
||||
storable.UpdatedAt, storable.UpdatedBy, storable.Source, storable.Data).
|
||||
Where("id = ?", storable.ID.StringValue()).
|
||||
Where("org_id = ?", storable.OrgID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "error in updating saved view")
|
||||
@@ -62,7 +65,7 @@ func (store *store) Update(ctx context.Context, view *savedviewtypes.SavedView)
|
||||
|
||||
func (store *store) Delete(ctx context.Context, orgID string, id valuer.UUID) error {
|
||||
res, err := store.sqlstore.BunDB().NewDelete().
|
||||
Model(&savedviewtypes.SavedView{}).
|
||||
Model((*savedviewtypes.StorableSavedView)(nil)).
|
||||
Where("id = ?", id.StringValue()).
|
||||
Where("org_id = ?", orgID).
|
||||
Exec(ctx)
|
||||
@@ -82,8 +85,8 @@ func (store *store) Delete(ctx context.Context, orgID string, id valuer.UUID) er
|
||||
}
|
||||
|
||||
func (store *store) List(ctx context.Context, orgID string, source savedviewtypes.Source, name string) ([]*savedviewtypes.SavedView, error) {
|
||||
var views []*savedviewtypes.SavedView
|
||||
q := store.sqlstore.BunDB().NewSelect().Model(&views).
|
||||
var storables []*savedviewtypes.StorableSavedView
|
||||
q := store.sqlstore.BunDB().NewSelect().Model(&storables).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("name LIKE ?", "%"+name+"%")
|
||||
if !source.IsZero() {
|
||||
@@ -94,8 +97,11 @@ func (store *store) List(ctx context.Context, orgID string, source savedviewtype
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in getting saved views")
|
||||
}
|
||||
|
||||
for _, view := range views {
|
||||
views := make([]*savedviewtypes.SavedView, 0, len(storables))
|
||||
for _, storable := range storables {
|
||||
view := storable.ToSavedView()
|
||||
normalizeSelectedFields(view)
|
||||
views = append(views, view)
|
||||
}
|
||||
|
||||
return views, nil
|
||||
@@ -103,7 +109,7 @@ func (store *store) List(ctx context.Context, orgID string, source savedviewtype
|
||||
|
||||
// normalizeSelectedFields fixes up a scanned row's nil SelectedFields.
|
||||
func normalizeSelectedFields(view *savedviewtypes.SavedView) {
|
||||
if view.Data.Spec.SelectedFields == nil {
|
||||
view.Data.Spec.SelectedFields = []telemetrytypes.TelemetryFieldKey{}
|
||||
if view.Spec.SelectedFields == nil {
|
||||
view.Spec.SelectedFields = []telemetrytypes.TelemetryFieldKey{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,58 @@ func (handler *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Success(rw, http.StatusCreated, invites[0])
|
||||
}
|
||||
|
||||
func (handler *handler) CreateBulkInvite(rw 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(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
var req types.PostableBulkInviteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that the request contains users
|
||||
if len(req.Invites) == 0 {
|
||||
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "no invites provided for invitation"))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.setter.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.IdentityID()), valuer.MustNewEmail(claims.Email), &req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) GetUserDeprecated(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
|
||||
}
|
||||
|
||||
user, err := handler.getter.GetDeprecatedUserByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(id))
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, user)
|
||||
}
|
||||
|
||||
func (handler *handler) GetUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
@@ -232,6 +284,33 @@ func (handler *handler) ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
render.Success(w, http.StatusOK, users)
|
||||
}
|
||||
|
||||
func (handler *handler) UpdateUserDeprecated(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
|
||||
}
|
||||
|
||||
user := types.DeprecatedUser{User: &types.User{}}
|
||||
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedUser, err := handler.setter.UpdateUserDeprecated(ctx, valuer.MustNewUUID(claims.OrgID), id, &user)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, updatedUser)
|
||||
}
|
||||
|
||||
func (handler *handler) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -276,6 +276,92 @@ func (module *setter) CreatePendingInviteUser(ctx context.Context, identityID va
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (module *setter) UpdateUserDeprecated(ctx context.Context, orgID valuer.UUID, id string, user *types.DeprecatedUser) (*types.DeprecatedUser, error) {
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingUser, err := module.getter.GetDeprecatedUserByOrgIDAndID(ctx, orgID, valuer.MustNewUUID(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := existingUser.ErrIfRoot(); err != nil {
|
||||
return nil, errors.WithAdditionalf(err, "cannot update root user")
|
||||
}
|
||||
|
||||
if err := existingUser.ErrIfDeleted(); err != nil {
|
||||
return nil, errors.WithAdditionalf(err, "cannot update deleted user")
|
||||
}
|
||||
|
||||
roleChange := user.Role != "" && user.Role != existingUser.Role
|
||||
|
||||
if roleChange {
|
||||
selectors := []coretypes.Selector{
|
||||
coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName),
|
||||
}
|
||||
err = module.authz.CheckWithTupleCreation(
|
||||
ctx,
|
||||
claims,
|
||||
valuer.MustNewUUID(claims.OrgID),
|
||||
authtypes.Relation{Verb: coretypes.VerbAssignee},
|
||||
coretypes.NewResourceRole(),
|
||||
selectors,
|
||||
selectors,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "only admins can change roles")
|
||||
}
|
||||
}
|
||||
|
||||
// make sure the user is not demoting self from admin
|
||||
if roleChange && existingUser.ID == valuer.MustNewUUID(claims.IdentityID()) && existingUser.Role == types.RoleAdmin && user.Role != types.RoleAdmin {
|
||||
return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot change self role")
|
||||
}
|
||||
|
||||
if roleChange {
|
||||
err = module.authz.ModifyGrant(ctx,
|
||||
orgID,
|
||||
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(existingUser.Role)},
|
||||
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
|
||||
authtypes.MustNewSubject(coretypes.NewResourceUser(), id, orgID, nil),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
existingUser.Update(user.DisplayName, user.Role)
|
||||
|
||||
// update the user - idempotent (this does analytics too so keeping it outside txn)
|
||||
if err := module.UpdateAnyUserDeprecated(ctx, orgID, existingUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if roleChange {
|
||||
// delete old role entries and create new ones
|
||||
if err := module.userRoleStore.DeleteUserRoles(ctx, existingUser.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create new ones
|
||||
if err := module.createUserRoleEntries(ctx, existingUser.OrgID, existingUser.ID, []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return existingUser, nil
|
||||
}
|
||||
|
||||
func (module *setter) UpdateUser(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, updatable *types.UpdatableUser) (*types.User, error) {
|
||||
existingUser, err := module.getter.GetUserByOrgIDAndID(ctx, orgID, userID)
|
||||
if err != nil {
|
||||
@@ -315,6 +401,23 @@ func (module *setter) UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *setter) UpdateAnyUserDeprecated(ctx context.Context, orgID valuer.UUID, deprecateUser *types.DeprecatedUser) error {
|
||||
user := types.NewUserFromDeprecatedUser(deprecateUser)
|
||||
if err := module.store.UpdateUser(ctx, orgID, user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
traits := types.NewTraitsFromDeprecatedUser(deprecateUser)
|
||||
module.analytics.IdentifyUser(ctx, user.OrgID.String(), user.ID.String(), traits)
|
||||
module.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Updated", traits)
|
||||
|
||||
if err := module.tokenizer.DeleteIdentity(ctx, user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *setter) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error {
|
||||
user, err := module.store.GetUser(ctx, valuer.MustNewUUID(id))
|
||||
if err != nil {
|
||||
|
||||
@@ -34,9 +34,11 @@ type Setter interface {
|
||||
// Initiate forgot password flow for a user
|
||||
ForgotPassword(ctx context.Context, orgID valuer.UUID, email valuer.Email, frontendBaseURL string) error
|
||||
|
||||
UpdateUserDeprecated(ctx context.Context, orgID valuer.UUID, id string, user *types.DeprecatedUser) (*types.DeprecatedUser, error)
|
||||
UpdateUser(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, updatable *types.UpdatableUser) (*types.User, error)
|
||||
|
||||
// UpdateAnyUser updates a user and persists the changes to the database along with the analytics and identity deletion.
|
||||
UpdateAnyUserDeprecated(ctx context.Context, orgID valuer.UUID, deprecateUser *types.DeprecatedUser) error
|
||||
UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user *types.User) error
|
||||
DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error
|
||||
|
||||
@@ -107,13 +109,16 @@ type Getter interface {
|
||||
type Handler interface {
|
||||
// invite
|
||||
CreateInvite(http.ResponseWriter, *http.Request)
|
||||
CreateBulkInvite(http.ResponseWriter, *http.Request)
|
||||
|
||||
// users
|
||||
ListUsersDeprecated(http.ResponseWriter, *http.Request)
|
||||
ListUsers(http.ResponseWriter, *http.Request)
|
||||
CreateUser(http.ResponseWriter, *http.Request)
|
||||
UpdateUserDeprecated(http.ResponseWriter, *http.Request)
|
||||
UpdateUser(http.ResponseWriter, *http.Request)
|
||||
DeleteUser(http.ResponseWriter, *http.Request)
|
||||
GetUserDeprecated(http.ResponseWriter, *http.Request)
|
||||
GetUser(http.ResponseWriter, *http.Request)
|
||||
GetMyUserDeprecated(http.ResponseWriter, *http.Request)
|
||||
GetMyUser(http.ResponseWriter, *http.Request)
|
||||
|
||||
@@ -237,6 +237,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddDashboardTuplesFactory(sqlstore),
|
||||
sqlmigration.NewRestructureSavedViewSpecFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddSavedViewTuplesFactory(sqlstore),
|
||||
sqlmigration.NewFixSavedViewSelectedFieldsFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
type restructureSavedViewSpec struct {
|
||||
@@ -44,11 +45,11 @@ type legacySavedViewCompositeQuery struct {
|
||||
|
||||
// legacySavedViewExtraData mirrors the frontend defined extraData JSON shape.
|
||||
type legacySavedViewExtraData struct {
|
||||
Color string `json:"color,omitempty"`
|
||||
SelectColumns json.RawMessage `json:"selectColumns,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
MaxLines int `json:"maxLines,omitempty"`
|
||||
FontSize string `json:"fontSize,omitempty"`
|
||||
Color string `json:"color,omitempty"`
|
||||
SelectColumns []telemetrytypes.TelemetryFieldKey `json:"selectColumns,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
MaxLines int `json:"maxLines,omitempty"`
|
||||
FontSize string `json:"fontSize,omitempty"`
|
||||
}
|
||||
|
||||
type savedViewDisplay struct {
|
||||
@@ -59,11 +60,11 @@ type savedViewDisplay struct {
|
||||
}
|
||||
|
||||
type savedViewSpec struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
PanelType string `json:"panelType"`
|
||||
Queries json.RawMessage `json:"queries"`
|
||||
SelectedFields json.RawMessage `json:"selectedFields"`
|
||||
Display savedViewDisplay `json:"display"`
|
||||
DisplayName string `json:"displayName"`
|
||||
PanelType string `json:"panelType"`
|
||||
Queries json.RawMessage `json:"queries"`
|
||||
SelectedFields []telemetrytypes.TelemetryFieldKey `json:"selectedFields"`
|
||||
Display savedViewDisplay `json:"display"`
|
||||
}
|
||||
|
||||
type savedViewData struct {
|
||||
@@ -238,7 +239,9 @@ func (migration *restructureSavedViewSpec) Up(ctx context.Context, db *bun.DB) e
|
||||
if old.ExtraData != "" {
|
||||
// best-effort: malformed/older extraData shapes never fail the migration,
|
||||
// they just leave selectedFields/display empty.
|
||||
_ = json.Unmarshal([]byte(old.ExtraData), &extraData)
|
||||
if err := json.Unmarshal([]byte(old.ExtraData), &extraData); err != nil {
|
||||
migration.settings.Logger.WarnContext(ctx, "failed to unmarshal saved view extra data, continuing with empty selectedFields/display", slog.String("org_id", old.OrgID), slog.String("saved_view_id", old.ID), slog.Any("error", err))
|
||||
}
|
||||
}
|
||||
|
||||
dataJSON, err := json.Marshal(savedViewData{
|
||||
|
||||
185
pkg/sqlmigration/111_fix_saved_view_selected_fields.go
Normal file
185
pkg/sqlmigration/111_fix_saved_view_selected_fields.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
type fixSavedViewSelectedFields struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
settings factory.ProviderSettings
|
||||
}
|
||||
|
||||
func NewFixSavedViewSelectedFieldsFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("fix_saved_view_selected_fields"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &fixSavedViewSelectedFields{sqlstore: sqlstore, settings: ps}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *fixSavedViewSelectedFields) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
// storableSavedViewData is the shape of the `saved_view` table this migration repairs.
|
||||
type storableSavedViewData struct {
|
||||
bun.BaseModel `bun:"table:saved_view"`
|
||||
|
||||
ID string `bun:"id,pk,type:text"`
|
||||
Data string `bun:"data,type:text"`
|
||||
}
|
||||
|
||||
// specFieldUnmarshalsCleanly reports whether value can be unmarshalled into
|
||||
// the real type of the given savedviewtypes.SavedViewSpec JSON key.
|
||||
func specFieldUnmarshalsCleanly(key string, value json.RawMessage) bool {
|
||||
switch key {
|
||||
case "displayName", "panelType":
|
||||
var s string
|
||||
return json.Unmarshal(value, &s) == nil
|
||||
case "queries":
|
||||
var q []qbtypes.QueryEnvelope
|
||||
return json.Unmarshal(value, &q) == nil
|
||||
case "selectedFields":
|
||||
var f []telemetrytypes.TelemetryFieldKey
|
||||
return json.Unmarshal(value, &f) == nil
|
||||
case "display":
|
||||
var d savedviewtypes.Display
|
||||
return json.Unmarshal(value, &d) == nil
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// specFieldZeroValueJSON is the JSON to substitute for a spec key that fails
|
||||
// to unmarshal into its real type.
|
||||
var specFieldZeroValueJSON = map[string]string{
|
||||
"displayName": `""`,
|
||||
"panelType": `""`,
|
||||
"queries": `[]`,
|
||||
"selectedFields": `[]`,
|
||||
"display": `{}`,
|
||||
}
|
||||
|
||||
// repairSavedViewData tries to make data unmarshal cleanly into
|
||||
// savedviewtypes.SavedViewData by blanking, one key at a time, whichever
|
||||
// top-level spec fields fail to unmarshal into their real type -- e.g. a
|
||||
// selectedFields shape the 109 migration forwarded verbatim from a
|
||||
// pre-telemetrytypes.TelemetryFieldKey install, or a queries shape that
|
||||
// predates the current discriminated-union QueryEnvelope. Every other key is
|
||||
// left byte-for-byte untouched. Returns ok=false if data/spec aren't even
|
||||
// JSON objects, or the result still doesn't unmarshal cleanly afterward.
|
||||
func repairSavedViewData(data string) (fixed string, blanked []string, ok bool) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal([]byte(data), &raw); err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
var spec map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw["spec"], &spec); err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
for key, value := range spec {
|
||||
if specFieldUnmarshalsCleanly(key, value) {
|
||||
continue
|
||||
}
|
||||
zero, known := specFieldZeroValueJSON[key]
|
||||
if !known {
|
||||
continue
|
||||
}
|
||||
spec[key] = json.RawMessage(zero)
|
||||
blanked = append(blanked, key)
|
||||
}
|
||||
|
||||
fixedSpec, err := json.Marshal(spec)
|
||||
if err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
raw["spec"] = fixedSpec
|
||||
|
||||
fixedData, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
// verify the fix actually round-trips through the real type before writing it.
|
||||
if err := json.Unmarshal(fixedData, new(savedviewtypes.SavedViewData)); err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
return string(fixedData), blanked, true
|
||||
}
|
||||
|
||||
// placeholderSavedViewData is substituted whole when a row can't be repaired
|
||||
// field-by-field (data/spec aren't JSON objects at all, or repair still
|
||||
// doesn't unmarshal cleanly). It must itself always unmarshal cleanly, since
|
||||
// every Get/List reads saved_view.data straight into savedviewtypes.SavedViewData
|
||||
// -- leaving genuinely-unrepairable data in place would 500 on every future read.
|
||||
func placeholderSavedViewData(id string) string {
|
||||
data, err := json.Marshal(savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion.StringValue(),
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: fmt.Sprintf("corrupted saved view %s", id),
|
||||
PanelType: savedviewtypes.PanelTypeTable,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
// marshalling a static, well-formed literal cannot fail.
|
||||
panic(err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (migration *fixSavedViewSelectedFields) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var rows []*storableSavedViewData
|
||||
if err := tx.NewSelect().Model(&rows).Scan(ctx); err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
||||
var repaired, replaced int
|
||||
for _, row := range rows {
|
||||
// already scans cleanly as-is -- nothing to repair.
|
||||
if err := json.Unmarshal([]byte(row.Data), new(savedviewtypes.SavedViewData)); err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fixedData, blanked, ok := repairSavedViewData(row.Data)
|
||||
if !ok {
|
||||
fixedData = placeholderSavedViewData(row.ID)
|
||||
replaced++
|
||||
migration.settings.Logger.WarnContext(ctx, "saved view data could not be repaired field-by-field, replacing with a placeholder view", slog.String("saved_view_id", row.ID))
|
||||
} else {
|
||||
repaired++
|
||||
migration.settings.Logger.WarnContext(ctx, "repaired saved view data by blanking fields that failed to unmarshal", slog.String("saved_view_id", row.ID), slog.Any("fields_blanked", blanked))
|
||||
}
|
||||
|
||||
if _, err := tx.NewUpdate().Model((*storableSavedViewData)(nil)).Set("data = ?", fixedData).Where("id = ?", row.ID).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
migration.settings.Logger.InfoContext(ctx, "checked saved views for unreadable data", slog.Int("total", len(rows)), slog.Int("repaired", repaired), slog.Int("replaced", replaced))
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *fixSavedViewSelectedFields) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -72,6 +72,14 @@ func (c *Claims) LogValue() slog.Value {
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Claims) IsSelfAccess(id string) error {
|
||||
if c.UserID == id {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.NewForbiddenf(errors.CodeForbidden, "only the user/admin can access their own resource")
|
||||
}
|
||||
|
||||
func (c *Claims) IdentityID() string {
|
||||
if c.Principal == PrincipalUser {
|
||||
return c.UserID
|
||||
|
||||
@@ -27,28 +27,75 @@ var (
|
||||
SourceMeter = Source{valuer.NewString("meter")}
|
||||
)
|
||||
|
||||
// SavedView is the domain/wire shape -- schemaVersion and spec are promoted
|
||||
// to the top level, matching dashboardtypes.DashboardV2 and ruletypes'
|
||||
// v2alpha1 rules. StorableSavedView is the distinct bun-mapped row shape;
|
||||
// the two diverge because bun maps Data to a single opaque `data` column,
|
||||
// which is incompatible with promoting its fields to the top level for JSON.
|
||||
type SavedView struct {
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
OrgID string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Source Source `json:"source"`
|
||||
SchemaVersion SchemaVersion `json:"schemaVersion" required:"true"`
|
||||
Spec SavedViewSpec `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
// StorableSavedView is the row shape bun maps to the saved_view table.
|
||||
type StorableSavedView struct {
|
||||
bun.BaseModel `bun:"table:saved_view"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
OrgID string `json:"-" bun:"org_id,notnull"`
|
||||
Name string `json:"name" bun:"name,type:text,notnull"`
|
||||
Source Source `json:"source" bun:"source,type:text,notnull"`
|
||||
Data SavedViewData `json:"data" bun:"data,type:text,notnull"`
|
||||
OrgID string `bun:"org_id,notnull"`
|
||||
Name string `bun:"name,type:text,notnull"`
|
||||
Source Source `bun:"source,type:text,notnull"`
|
||||
Data SavedViewData `bun:"data,type:text,notnull"`
|
||||
}
|
||||
|
||||
func (s *StorableSavedView) ToSavedView() *SavedView {
|
||||
return &SavedView{
|
||||
Identifiable: s.Identifiable,
|
||||
TimeAuditable: s.TimeAuditable,
|
||||
UserAuditable: s.UserAuditable,
|
||||
OrgID: s.OrgID,
|
||||
Name: s.Name,
|
||||
Source: s.Source,
|
||||
SchemaVersion: SchemaVersion{valuer.NewString(s.Data.SchemaVersion)},
|
||||
Spec: s.Data.Spec,
|
||||
}
|
||||
}
|
||||
|
||||
func NewStorableSavedView(view *SavedView) *StorableSavedView {
|
||||
return &StorableSavedView{
|
||||
Identifiable: view.Identifiable,
|
||||
TimeAuditable: view.TimeAuditable,
|
||||
UserAuditable: view.UserAuditable,
|
||||
OrgID: view.OrgID,
|
||||
Name: view.Name,
|
||||
Source: view.Source,
|
||||
Data: SavedViewData{
|
||||
SchemaVersion: view.SchemaVersion.StringValue(),
|
||||
Spec: view.Spec,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type PostableSavedView struct {
|
||||
Name string `json:"name"`
|
||||
GenerateName bool `json:"generateName"`
|
||||
Source Source `json:"source" required:"true"`
|
||||
Data SavedViewData `json:"data" required:"true"`
|
||||
Name string `json:"name"`
|
||||
GenerateName bool `json:"generateName"`
|
||||
Source Source `json:"source" required:"true"`
|
||||
SchemaVersion SchemaVersion `json:"schemaVersion" required:"true"`
|
||||
Spec SavedViewSpec `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
type UpdatableSavedView struct {
|
||||
Source Source `json:"source" required:"true"`
|
||||
Data SavedViewData `json:"data" required:"true"`
|
||||
Source Source `json:"source" required:"true"`
|
||||
SchemaVersion SchemaVersion `json:"schemaVersion" required:"true"`
|
||||
Spec SavedViewSpec `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
type ListSavedViewsParams struct {
|
||||
@@ -83,7 +130,7 @@ func (postable PostableSavedView) ToSavedView(orgID string, createdBy string) *S
|
||||
|
||||
name := postable.Name
|
||||
if postable.GenerateName {
|
||||
name = generateSavedViewName(postable.Data.Spec.DisplayName)
|
||||
name = generateSavedViewName(postable.Spec.DisplayName)
|
||||
}
|
||||
|
||||
return &SavedView{
|
||||
@@ -93,7 +140,8 @@ func (postable PostableSavedView) ToSavedView(orgID string, createdBy string) *S
|
||||
OrgID: orgID,
|
||||
Name: name,
|
||||
Source: postable.Source,
|
||||
Data: postable.Data,
|
||||
SchemaVersion: postable.SchemaVersion,
|
||||
Spec: postable.Spec,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +154,8 @@ func (updatable UpdatableSavedView) ToSavedView(id valuer.UUID, orgID string, up
|
||||
UserAuditable: types.UserAuditable{UpdatedBy: updatedBy},
|
||||
OrgID: orgID,
|
||||
Source: updatable.Source,
|
||||
Data: updatable.Data,
|
||||
SchemaVersion: updatable.SchemaVersion,
|
||||
Spec: updatable.Spec,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,8 +166,11 @@ func (p *PostableSavedView) Validate() error {
|
||||
if err := p.Source.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.SchemaVersion.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Data.Validate()
|
||||
return p.Spec.Validate()
|
||||
}
|
||||
|
||||
func (p *PostableSavedView) validateName() error {
|
||||
@@ -135,8 +187,11 @@ func (u *UpdatableSavedView) Validate() error {
|
||||
if err := u.Source.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := u.SchemaVersion.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return u.Data.Validate()
|
||||
return u.Spec.Validate()
|
||||
}
|
||||
|
||||
func (p *ListSavedViewsParams) Validate() error {
|
||||
|
||||
@@ -11,22 +11,18 @@ import (
|
||||
|
||||
func validPostableSavedView() PostableSavedView {
|
||||
return PostableSavedView{
|
||||
Name: "my-view",
|
||||
Source: SourceLogs,
|
||||
Data: SavedViewData{
|
||||
SchemaVersion: SavedViewSchemaVersion,
|
||||
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
|
||||
},
|
||||
Name: "my-view",
|
||||
Source: SourceLogs,
|
||||
SchemaVersion: SavedViewSchemaVersion,
|
||||
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
|
||||
}
|
||||
}
|
||||
|
||||
func validUpdatableSavedView() UpdatableSavedView {
|
||||
return UpdatableSavedView{
|
||||
Source: SourceLogs,
|
||||
Data: SavedViewData{
|
||||
SchemaVersion: SavedViewSchemaVersion,
|
||||
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
|
||||
},
|
||||
Source: SourceLogs,
|
||||
SchemaVersion: SavedViewSchemaVersion,
|
||||
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +65,7 @@ func TestPostableSavedViewValidate(t *testing.T) {
|
||||
|
||||
t.Run("invalid saved view data is rejected", func(t *testing.T) {
|
||||
view := validPostableSavedView()
|
||||
view.Data.SchemaVersion = "v1"
|
||||
view.SchemaVersion = SchemaVersion{valuer.NewString("v1")}
|
||||
assert.Error(t, view.Validate())
|
||||
})
|
||||
|
||||
@@ -100,7 +96,7 @@ func TestPostableSavedViewValidate(t *testing.T) {
|
||||
|
||||
t.Run("empty displayName is rejected", func(t *testing.T) {
|
||||
view := validPostableSavedView()
|
||||
view.Data.Spec.DisplayName = ""
|
||||
view.Spec.DisplayName = ""
|
||||
assert.ErrorContains(t, view.Validate(), "displayName is required")
|
||||
})
|
||||
}
|
||||
@@ -119,7 +115,7 @@ func TestUpdatableSavedViewValidate(t *testing.T) {
|
||||
|
||||
t.Run("empty displayName is rejected", func(t *testing.T) {
|
||||
view := validUpdatableSavedView()
|
||||
view.Data.Spec.DisplayName = ""
|
||||
view.Spec.DisplayName = ""
|
||||
assert.ErrorContains(t, view.Validate(), "displayName is required")
|
||||
})
|
||||
}
|
||||
@@ -153,7 +149,8 @@ func TestNewSavedView(t *testing.T) {
|
||||
assert.Equal(t, "creator@signoz.io", savedView.UpdatedBy)
|
||||
assert.Equal(t, view.Name, savedView.Name)
|
||||
assert.Equal(t, view.Source, savedView.Source)
|
||||
assert.Equal(t, view.Data, savedView.Data)
|
||||
assert.Equal(t, view.SchemaVersion, savedView.SchemaVersion)
|
||||
assert.Equal(t, view.Spec, savedView.Spec)
|
||||
assert.False(t, savedView.CreatedAt.IsZero())
|
||||
assert.Equal(t, savedView.CreatedAt, savedView.UpdatedAt)
|
||||
}
|
||||
@@ -163,14 +160,14 @@ func TestNewSavedView_GeneratesNameWhenEmpty(t *testing.T) {
|
||||
view := validPostableSavedView()
|
||||
view.Name = ""
|
||||
view.GenerateName = true
|
||||
view.Data.Spec.DisplayName = "My View!"
|
||||
view.Spec.DisplayName = "My View!"
|
||||
|
||||
savedView := view.ToSavedView(orgID, "creator@signoz.io")
|
||||
|
||||
assert.NotEmpty(t, savedView.Name)
|
||||
assert.Empty(t, validation.IsDNS1123Label(savedView.Name), "generated name must be a valid DNS-1123 label")
|
||||
assert.True(t, strings.HasPrefix(savedView.Name, "my-view-"))
|
||||
assert.Equal(t, "My View!", savedView.Data.Spec.DisplayName)
|
||||
assert.Equal(t, "My View!", savedView.Spec.DisplayName)
|
||||
}
|
||||
|
||||
func TestGenerateSavedViewName(t *testing.T) {
|
||||
|
||||
@@ -28,7 +28,7 @@ func (t *StoreTest) Store() savedviewtypes.Store { return t.store }
|
||||
func (t *StoreTest) Mock() sqlmock.Sqlmock { return t.mock }
|
||||
|
||||
func savedViewRow(view *savedviewtypes.SavedView) []driver.Value {
|
||||
data, _ := json.Marshal(view.Data)
|
||||
data, _ := json.Marshal(savedviewtypes.NewStorableSavedView(view).Data)
|
||||
return []driver.Value{
|
||||
view.ID.StringValue(),
|
||||
view.CreatedAt,
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// SavedViewSchemaVersion is the only schemaVersion currently.
|
||||
const SavedViewSchemaVersion = "v2"
|
||||
var SavedViewSchemaVersion = SchemaVersion{valuer.NewString("v2")}
|
||||
|
||||
var (
|
||||
PanelTypeValue = PanelType{valuer.NewString("value")}
|
||||
@@ -30,9 +30,9 @@ type Display struct {
|
||||
type SavedViewSpec struct {
|
||||
DisplayName string `json:"displayName" required:"true"`
|
||||
PanelType PanelType `json:"panelType" required:"true"`
|
||||
Queries []qbtypes.QueryEnvelope `json:"queries" required:"true" nullable:"false"`
|
||||
SelectedFields []telemetrytypes.TelemetryFieldKey `json:"selectedFields" required:"true" nullable:"false"`
|
||||
Display Display `json:"display" required:"true"`
|
||||
Queries []qbtypes.QueryEnvelope `json:"queries" required:"true" nullable:"false" minItems:"1"`
|
||||
SelectedFields []telemetrytypes.TelemetryFieldKey `json:"selectedFields" nullable:"false"`
|
||||
Display Display `json:"display"`
|
||||
}
|
||||
|
||||
// SavedViewData is what's persisted as saved view data.
|
||||
@@ -41,6 +41,11 @@ type SavedViewData struct {
|
||||
Spec SavedViewSpec `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
// SchemaVersion has v2 as the only value currently.
|
||||
type SchemaVersion struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
// PanelType is the explore-page panel a saved view renders as.
|
||||
type PanelType struct {
|
||||
valuer.String
|
||||
@@ -65,6 +70,17 @@ func (p PanelType) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (SchemaVersion) Enum() []any {
|
||||
return []any{SavedViewSchemaVersion}
|
||||
}
|
||||
|
||||
func (s SchemaVersion) Validate() error {
|
||||
if s != SavedViewSchemaVersion {
|
||||
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "schemaVersion must be %q, got %q", SavedViewSchemaVersion.StringValue(), s.StringValue())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SavedViewSpec) Validate() error {
|
||||
if s.DisplayName == "" {
|
||||
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "displayName is required")
|
||||
@@ -75,11 +91,3 @@ func (s *SavedViewSpec) Validate() error {
|
||||
|
||||
return (&qbtypes.CompositeQuery{Queries: s.Queries}).Validate()
|
||||
}
|
||||
|
||||
func (d *SavedViewData) Validate() error {
|
||||
if d.SchemaVersion != SavedViewSchemaVersion {
|
||||
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "schemaVersion must be %q, got %q", SavedViewSchemaVersion, d.SchemaVersion)
|
||||
}
|
||||
|
||||
return d.Spec.Validate()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package savedviewtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func validQueries() []qbtypes.QueryEnvelope {
|
||||
@@ -75,7 +78,7 @@ func TestSavedViewSpecValidate(t *testing.T) {
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "selected fields and display are not required",
|
||||
name: "selectedFields and display populated is still valid",
|
||||
spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeTable,
|
||||
@@ -85,6 +88,36 @@ func TestSavedViewSpecValidate(t *testing.T) {
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "nil selectedFields is valid -- neither field is actually required",
|
||||
spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeTable,
|
||||
Queries: validQueries(),
|
||||
SelectedFields: nil,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty (non-nil) selectedFields is valid",
|
||||
spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeTable,
|
||||
Queries: validQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "zero-value display is valid",
|
||||
spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeTable,
|
||||
Queries: validQueries(),
|
||||
Display: Display{},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -99,37 +132,44 @@ func TestSavedViewSpecValidate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavedViewDataValidate(t *testing.T) {
|
||||
func TestSavedViewSpecJSONUnmarshal_OptionalFields(t *testing.T) {
|
||||
base := `"displayName":"My View","panelType":"table","queries":[{"type":"builder_query","spec":{"signal":"logs","aggregations":[{"expression":"count()"}]}}]`
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
data SavedViewData
|
||||
expectError bool
|
||||
name string
|
||||
json string
|
||||
}{
|
||||
{
|
||||
name: "valid data",
|
||||
data: SavedViewData{SchemaVersion: SavedViewSchemaVersion, Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "wrong schema version is rejected",
|
||||
data: SavedViewData{SchemaVersion: "v1", Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()}},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "empty schema version is rejected",
|
||||
data: SavedViewData{Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()}},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid spec is rejected",
|
||||
data: SavedViewData{SchemaVersion: SavedViewSchemaVersion, Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph}},
|
||||
expectError: true,
|
||||
},
|
||||
{name: "selectedFields and display omitted entirely", json: `{` + base + `}`},
|
||||
{name: "selectedFields and display explicitly null", json: `{` + base + `,"selectedFields":null,"display":null}`},
|
||||
{name: "selectedFields empty array, display empty object", json: `{` + base + `,"selectedFields":[],"display":{}}`},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
err := c.data.Validate()
|
||||
var spec SavedViewSpec
|
||||
err := json.Unmarshal([]byte(c.json), &spec)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, spec.Validate())
|
||||
assert.Empty(t, spec.SelectedFields)
|
||||
assert.Equal(t, Display{}, spec.Display)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaVersionValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
schemaVersion SchemaVersion
|
||||
expectError bool
|
||||
}{
|
||||
{name: "valid schema version", schemaVersion: SavedViewSchemaVersion, expectError: false},
|
||||
{name: "wrong schema version is rejected", schemaVersion: SchemaVersion{valuer.NewString("v1")}, expectError: true},
|
||||
{name: "empty schema version is rejected", schemaVersion: SchemaVersion{}, expectError: true},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
err := c.schemaVersion.Validate()
|
||||
if c.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
|
||||
@@ -128,6 +128,18 @@ func NewDeprecatedUserFromUserAndRole(user *User, role Role) *DeprecatedUser {
|
||||
}
|
||||
}
|
||||
|
||||
func NewUserFromDeprecatedUser(deprecatedUser *DeprecatedUser) *User {
|
||||
return &User{
|
||||
Identifiable: deprecatedUser.Identifiable,
|
||||
DisplayName: deprecatedUser.DisplayName,
|
||||
Email: deprecatedUser.Email,
|
||||
OrgID: deprecatedUser.OrgID,
|
||||
IsRoot: deprecatedUser.IsRoot,
|
||||
Status: deprecatedUser.Status,
|
||||
TimeAuditable: deprecatedUser.TimeAuditable,
|
||||
}
|
||||
}
|
||||
|
||||
// Update applies mutable fields from the input to the user. Immutable fields
|
||||
// (email, is_root, org_id, id) are preserved. Only non-zero input fields are applied.
|
||||
func (u *User) Update(displayName string) {
|
||||
@@ -137,6 +149,16 @@ func (u *User) Update(displayName string) {
|
||||
u.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
func (u *DeprecatedUser) Update(displayName string, role Role) {
|
||||
if displayName != "" {
|
||||
u.DisplayName = displayName
|
||||
}
|
||||
if role != "" {
|
||||
u.Role = role
|
||||
}
|
||||
u.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
func (u *User) UpdateStatus(status valuer.String) error {
|
||||
// no updates allowed if user is in delete state
|
||||
if err := u.ErrIfDeleted(); err != nil {
|
||||
@@ -212,6 +234,17 @@ func NewTraitsFromUser(user *User) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func NewTraitsFromDeprecatedUser(user *DeprecatedUser) map[string]any {
|
||||
return map[string]any{
|
||||
"name": user.DisplayName,
|
||||
"role": user.Role,
|
||||
"email": user.Email.String(),
|
||||
"display_name": user.DisplayName,
|
||||
"status": user.Status,
|
||||
"created_at": user.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (request *PostableRegisterOrgAndAdmin) UnmarshalJSON(data []byte) error {
|
||||
type Alias PostableRegisterOrgAndAdmin
|
||||
|
||||
|
||||
@@ -566,7 +566,7 @@ def test_saml_sso_deleted_user_gets_new_user_on_login(
|
||||
|
||||
# --- Step 2: Soft delete via DB using API
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/user/{user_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
@@ -468,8 +468,8 @@ def test_pods_filter_pagination_and_ordering(
|
||||
crashloopbackoff matches 4 pods in pods_phases.jsonl: clbo-a, clbo-b, run-p, unk-p."""
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics(
|
||||
Metrics.load_from_file(
|
||||
get_testdata_file_path("inframonitoring/pods_phases.jsonl"),
|
||||
_load_pods_metrics(
|
||||
"inframonitoring/pods_phases.jsonl",
|
||||
base_time=now - timedelta(minutes=4),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -141,7 +141,7 @@ def test_revoke_invite(signoz: types.SigNoz, get_token: Callable[[str, str], str
|
||||
|
||||
# Delete the pending invite user (revoke the invite)
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{invited_user['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/user/{invited_user['id']}"),
|
||||
timeout=2,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
)
|
||||
|
||||
@@ -51,7 +51,7 @@ def test_reinvite_deleted_user(
|
||||
|
||||
# call the delete api which now soft deletes the user
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{invited_user['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/user/{invited_user['id']}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
@@ -90,6 +90,37 @@ def test_reinvite_deleted_user(
|
||||
assert user_token is not None
|
||||
|
||||
|
||||
def test_bulk_invite(
|
||||
signoz: SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""
|
||||
Verify the bulk invite endpoint creates multiple pending_invite users.
|
||||
"""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite/bulk"),
|
||||
json={
|
||||
"invites": [
|
||||
{
|
||||
"email": "bulk1@integration.test",
|
||||
"role": "EDITOR",
|
||||
"name": "bulk user 1",
|
||||
},
|
||||
{
|
||||
"email": "bulk2@integration.test",
|
||||
"role": "VIEWER",
|
||||
"name": "bulk user 2",
|
||||
},
|
||||
]
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
|
||||
|
||||
def test_delete_user(
|
||||
signoz: SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
@@ -123,7 +154,7 @@ def test_delete_user(
|
||||
|
||||
# delete the user
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/user/{user_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ def test_unique_index_allows_multiple_deleted_rows(
|
||||
first_user_id = resp.json()["data"]["id"]
|
||||
|
||||
resp = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{first_user_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/user/{first_user_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
@@ -76,7 +76,7 @@ def test_unique_index_allows_multiple_deleted_rows(
|
||||
assert second_user_id != first_user_id
|
||||
|
||||
resp = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{second_user_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/user/{second_user_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user