mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-05 18:10:31 +01:00
Compare commits
21 Commits
nv/v2-dash
...
nv/patch-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbba2e16d8 | ||
|
|
1d0ab788d5 | ||
|
|
4aae71462b | ||
|
|
bd49d94144 | ||
|
|
fa7205a673 | ||
|
|
13ec049495 | ||
|
|
3e8468ab23 | ||
|
|
e6cb7fabde | ||
|
|
2aa46f9f86 | ||
|
|
73fa15da83 | ||
|
|
cd70d0bdeb | ||
|
|
301d0103b0 | ||
|
|
dc99772ee4 | ||
|
|
80849ebfeb | ||
|
|
14927c89d3 | ||
|
|
55487dde3a | ||
|
|
fc5717af51 | ||
|
|
b3e3dd13b4 | ||
|
|
710d5531f3 | ||
|
|
ca96c71146 | ||
|
|
de2909d1d1 |
@@ -2524,6 +2524,42 @@ components:
|
||||
legend:
|
||||
$ref: '#/components/schemas/DashboardtypesLegend'
|
||||
type: object
|
||||
DashboardtypesJSONPatchDocument:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardtypesJSONPatchOperation'
|
||||
nullable: true
|
||||
type: array
|
||||
DashboardtypesJSONPatchOperation:
|
||||
properties:
|
||||
from:
|
||||
description: Source JSON Pointer for move/copy ops; ignored for other ops.
|
||||
type: string
|
||||
op:
|
||||
enum:
|
||||
- add
|
||||
- remove
|
||||
- replace
|
||||
- move
|
||||
- copy
|
||||
- test
|
||||
type: string
|
||||
path:
|
||||
description: JSON Pointer (RFC 6901) into the dashboard's postable shape
|
||||
— e.g. /data/display/name, /data/panels/<id>, /data/panels/<id>/spec/queries/0,
|
||||
/tags/-.
|
||||
type: string
|
||||
value:
|
||||
description: 'Value to add/replace/test against. The expected type depends
|
||||
on the path. Common shapes (see referenced schemas for the exact field
|
||||
set): /data/panels/<id> takes a DashboardtypesPanel; /data/panels/<id>/spec/queries/N
|
||||
(or /-) takes a DashboardtypesQuery; /data/variables/N takes a DashboardtypesVariable;
|
||||
/data/layouts/N takes a DashboardtypesLayout; /tags/N (or /-) takes a
|
||||
TagtypesPostableTag; /data/display/name and other leaf string fields take
|
||||
a string. Required for add/replace/test; ignored for remove/move/copy.'
|
||||
required:
|
||||
- op
|
||||
- path
|
||||
type: object
|
||||
DashboardtypesLayout:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpec'
|
||||
@@ -12048,6 +12084,66 @@ paths:
|
||||
summary: Get dashboard (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
patch:
|
||||
deprecated: false
|
||||
description: This endpoint applies an RFC 6902 JSON Patch to a v2-shape dashboard.
|
||||
The patch is applied against the postable view of the dashboard (metadata,
|
||||
data, tags), so individual panels, queries, variables, layouts, or tags can
|
||||
be updated without re-sending the rest of the dashboard. Locked dashboards
|
||||
are rejected.
|
||||
operationId: PatchDashboardV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DashboardtypesJSONPatchDocument'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesGettableDashboardV2'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Patch dashboard (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint updates a v2-shape dashboard's metadata, data, and
|
||||
@@ -12105,6 +12201,211 @@ paths:
|
||||
summary: Update dashboard (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/dashboards/{id}/lock:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: This endpoint unlocks a v2-shape dashboard. Only the dashboard's
|
||||
creator or an org admin may lock or unlock.
|
||||
operationId: UnlockDashboardV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: No Content
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Unlock dashboard (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint locks a v2-shape dashboard. Only the dashboard's
|
||||
creator or an org admin may lock or unlock.
|
||||
operationId: LockDashboardV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: No Content
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Lock dashboard (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/dashboards/{id}/public:
|
||||
patch:
|
||||
deprecated: false
|
||||
description: This endpoint creates the public sharing config for a v2 dashboard
|
||||
and returns the dashboard with the new public config attached. Lock state
|
||||
does not gate this endpoint.
|
||||
operationId: CreatePublicDashboardV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DashboardtypesPostablePublicDashboard'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesGettableDashboardV2'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Make a dashboard v2 public
|
||||
tags:
|
||||
- dashboard
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint updates the public sharing config (time range settings)
|
||||
of an already-public v2 dashboard. Lock state does not gate this endpoint.
|
||||
operationId: UpdatePublicDashboardV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DashboardtypesUpdatablePublicDashboard'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesGettableDashboardV2'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Update public sharing config for a dashboard v2
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/factor_password/forgot:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -211,6 +211,56 @@ func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer
|
||||
return module.pkgDashboardModule.UpdateV2(ctx, orgID, id, updatedBy, updateable)
|
||||
}
|
||||
|
||||
func (module *module) PatchV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, patch dashboardtypes.PatchableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
return module.pkgDashboardModule.PatchV2(ctx, orgID, id, updatedBy, patch)
|
||||
}
|
||||
|
||||
func (module *module) LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
|
||||
return module.pkgDashboardModule.LockUnlockV2(ctx, orgID, id, updatedBy, isAdmin, lock)
|
||||
}
|
||||
|
||||
func (module *module) CreatePublicV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, postable dashboardtypes.PostablePublicDashboard) (*dashboardtypes.DashboardV2, error) {
|
||||
if _, err := module.licensing.GetActive(ctx, orgID); err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
existing, err := module.pkgDashboardModule.GetV2(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing.PublicConfig != nil {
|
||||
return nil, errors.Newf(errors.TypeAlreadyExists, dashboardtypes.ErrCodePublicDashboardAlreadyExists, "dashboard with id %s is already public", id)
|
||||
}
|
||||
|
||||
publicDashboard := dashboardtypes.NewPublicDashboard(postable.TimeRangeEnabled, postable.DefaultTimeRange, id)
|
||||
if err := module.store.CreatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(publicDashboard)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existing.PublicConfig = publicDashboard
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
func (module *module) UpdatePublicV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatable dashboardtypes.UpdatablePublicDashboard) (*dashboardtypes.DashboardV2, error) {
|
||||
if _, err := module.licensing.GetActive(ctx, orgID); err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
existing, err := module.pkgDashboardModule.GetV2(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing.PublicConfig == nil {
|
||||
return nil, errors.Newf(errors.TypeNotFound, dashboardtypes.ErrCodePublicDashboardNotFound, "dashboard with id %s isn't public", id)
|
||||
}
|
||||
|
||||
existing.PublicConfig.Update(updatable.TimeRangeEnabled, updatable.DefaultTimeRange)
|
||||
if err := module.store.UpdatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(existing.PublicConfig)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
|
||||
return module.pkgDashboardModule.Get(ctx, orgID, id)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ import type {
|
||||
CreateDashboardV2201,
|
||||
CreatePublicDashboard201,
|
||||
CreatePublicDashboardPathParameters,
|
||||
CreatePublicDashboardV2200,
|
||||
CreatePublicDashboardV2PathParameters,
|
||||
DashboardtypesJSONPatchDocumentDTO,
|
||||
DashboardtypesPostableDashboardV2DTO,
|
||||
DashboardtypesPostablePublicDashboardDTO,
|
||||
DashboardtypesUpdatablePublicDashboardDTO,
|
||||
@@ -33,10 +36,16 @@ import type {
|
||||
GetPublicDashboardPathParameters,
|
||||
GetPublicDashboardWidgetQueryRange200,
|
||||
GetPublicDashboardWidgetQueryRangePathParameters,
|
||||
LockDashboardV2PathParameters,
|
||||
PatchDashboardV2200,
|
||||
PatchDashboardV2PathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
UnlockDashboardV2PathParameters,
|
||||
UpdateDashboardV2200,
|
||||
UpdateDashboardV2PathParameters,
|
||||
UpdatePublicDashboardPathParameters,
|
||||
UpdatePublicDashboardV2200,
|
||||
UpdatePublicDashboardV2PathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
@@ -828,6 +837,104 @@ export const invalidateGetDashboardV2 = async (
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint applies an RFC 6902 JSON Patch to a v2-shape dashboard. The patch is applied against the postable view of the dashboard (metadata, data, tags), so individual panels, queries, variables, layouts, or tags can be updated without re-sending the rest of the dashboard. Locked dashboards are rejected.
|
||||
* @summary Patch dashboard (v2)
|
||||
*/
|
||||
export const patchDashboardV2 = (
|
||||
{ id }: PatchDashboardV2PathParameters,
|
||||
dashboardtypesJSONPatchDocumentDTO: BodyType<DashboardtypesJSONPatchDocumentDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<PatchDashboardV2200>({
|
||||
url: `/api/v2/dashboards/${id}`,
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: dashboardtypesJSONPatchDocumentDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPatchDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesJSONPatchDocumentDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesJSONPatchDocumentDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['patchDashboardV2'];
|
||||
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 patchDashboardV2>>,
|
||||
{
|
||||
pathParams: PatchDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesJSONPatchDocumentDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return patchDashboardV2(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type PatchDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>
|
||||
>;
|
||||
export type PatchDashboardV2MutationBody =
|
||||
BodyType<DashboardtypesJSONPatchDocumentDTO>;
|
||||
export type PatchDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Patch dashboard (v2)
|
||||
*/
|
||||
export const usePatchDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesJSONPatchDocumentDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesJSONPatchDocumentDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getPatchDashboardV2MutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint updates a v2-shape dashboard's metadata, data, and tag set. Locked dashboards are rejected.
|
||||
* @summary Update dashboard (v2)
|
||||
@@ -926,3 +1033,355 @@ export const useUpdateDashboardV2 = <
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint unlocks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.
|
||||
* @summary Unlock dashboard (v2)
|
||||
*/
|
||||
export const unlockDashboardV2 = ({ id }: UnlockDashboardV2PathParameters) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v2/dashboards/${id}/lock`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const getUnlockDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnlockDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnlockDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['unlockDashboardV2'];
|
||||
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 unlockDashboardV2>>,
|
||||
{ pathParams: UnlockDashboardV2PathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return unlockDashboardV2(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UnlockDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>
|
||||
>;
|
||||
|
||||
export type UnlockDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Unlock dashboard (v2)
|
||||
*/
|
||||
export const useUnlockDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnlockDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnlockDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getUnlockDashboardV2MutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint locks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.
|
||||
* @summary Lock dashboard (v2)
|
||||
*/
|
||||
export const lockDashboardV2 = ({ id }: LockDashboardV2PathParameters) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v2/dashboards/${id}/lock`,
|
||||
method: 'PUT',
|
||||
});
|
||||
};
|
||||
|
||||
export const getLockDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: LockDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: LockDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['lockDashboardV2'];
|
||||
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 lockDashboardV2>>,
|
||||
{ pathParams: LockDashboardV2PathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return lockDashboardV2(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type LockDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>
|
||||
>;
|
||||
|
||||
export type LockDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Lock dashboard (v2)
|
||||
*/
|
||||
export const useLockDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: LockDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: LockDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getLockDashboardV2MutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint creates the public sharing config for a v2 dashboard and returns the dashboard with the new public config attached. Lock state does not gate this endpoint.
|
||||
* @summary Make a dashboard v2 public
|
||||
*/
|
||||
export const createPublicDashboardV2 = (
|
||||
{ id }: CreatePublicDashboardV2PathParameters,
|
||||
dashboardtypesPostablePublicDashboardDTO: BodyType<DashboardtypesPostablePublicDashboardDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreatePublicDashboardV2200>({
|
||||
url: `/api/v2/dashboards/${id}/public`,
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: dashboardtypesPostablePublicDashboardDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreatePublicDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createPublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesPostablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createPublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesPostablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createPublicDashboardV2'];
|
||||
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 createPublicDashboardV2>>,
|
||||
{
|
||||
pathParams: CreatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesPostablePublicDashboardDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return createPublicDashboardV2(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreatePublicDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createPublicDashboardV2>>
|
||||
>;
|
||||
export type CreatePublicDashboardV2MutationBody =
|
||||
BodyType<DashboardtypesPostablePublicDashboardDTO>;
|
||||
export type CreatePublicDashboardV2MutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Make a dashboard v2 public
|
||||
*/
|
||||
export const useCreatePublicDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createPublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesPostablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createPublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesPostablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getCreatePublicDashboardV2MutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint updates the public sharing config (time range settings) of an already-public v2 dashboard. Lock state does not gate this endpoint.
|
||||
* @summary Update public sharing config for a dashboard v2
|
||||
*/
|
||||
export const updatePublicDashboardV2 = (
|
||||
{ id }: UpdatePublicDashboardV2PathParameters,
|
||||
dashboardtypesUpdatablePublicDashboardDTO: BodyType<DashboardtypesUpdatablePublicDashboardDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<UpdatePublicDashboardV2200>({
|
||||
url: `/api/v2/dashboards/${id}/public`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: dashboardtypesUpdatablePublicDashboardDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdatePublicDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updatePublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesUpdatablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updatePublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesUpdatablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updatePublicDashboardV2'];
|
||||
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 updatePublicDashboardV2>>,
|
||||
{
|
||||
pathParams: UpdatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesUpdatablePublicDashboardDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updatePublicDashboardV2(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdatePublicDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updatePublicDashboardV2>>
|
||||
>;
|
||||
export type UpdatePublicDashboardV2MutationBody =
|
||||
BodyType<DashboardtypesUpdatablePublicDashboardDTO>;
|
||||
export type UpdatePublicDashboardV2MutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update public sharing config for a dashboard v2
|
||||
*/
|
||||
export const useUpdatePublicDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updatePublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesUpdatablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updatePublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesUpdatablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getUpdatePublicDashboardV2MutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
|
||||
@@ -4574,6 +4574,43 @@ export interface DashboardtypesHistogramPanelSpecDTO {
|
||||
legend?: DashboardtypesLegendDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type DashboardtypesJSONPatchDocumentDTO =
|
||||
| DashboardtypesJSONPatchOperationDTO[]
|
||||
| null;
|
||||
|
||||
export enum DashboardtypesJSONPatchOperationDTOOp {
|
||||
add = 'add',
|
||||
remove = 'remove',
|
||||
replace = 'replace',
|
||||
move = 'move',
|
||||
copy = 'copy',
|
||||
test = 'test',
|
||||
}
|
||||
export interface DashboardtypesJSONPatchOperationDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @description Source JSON Pointer for move/copy ops; ignored for other ops.
|
||||
*/
|
||||
from?: string;
|
||||
/**
|
||||
* @enum add,remove,replace,move,copy,test
|
||||
* @type string
|
||||
*/
|
||||
op: DashboardtypesJSONPatchOperationDTOOp;
|
||||
/**
|
||||
* @type string
|
||||
* @description JSON Pointer (RFC 6901) into the dashboard's postable shape — e.g. /data/display/name, /data/panels/<id>, /data/panels/<id>/spec/queries/0, /tags/-.
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* @description Value to add/replace/test against. The expected type depends on the path — a string for /data/display/name, a Panel for /data/panels/<id>, a PostableTag for /tags/-, etc. Required for add/replace/test; ignored for remove/move/copy.
|
||||
*/
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
export type DashboardtypesLayoutDTO =
|
||||
DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpecDTO;
|
||||
|
||||
@@ -10026,6 +10063,17 @@ export type GetDashboardV2200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type PatchDashboardV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type PatchDashboardV2200 = {
|
||||
data: DashboardtypesGettableDashboardV2DTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateDashboardV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
@@ -10037,6 +10085,34 @@ export type UpdateDashboardV2200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UnlockDashboardV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type LockDashboardV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type CreatePublicDashboardV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type CreatePublicDashboardV2200 = {
|
||||
data: DashboardtypesGettableDashboardV2DTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdatePublicDashboardV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type UpdatePublicDashboardV2200 = {
|
||||
data: DashboardtypesGettableDashboardV2DTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetFeatures200 = {
|
||||
/**
|
||||
* @type array
|
||||
|
||||
1
go.mod
1
go.mod
@@ -88,6 +88,7 @@ require (
|
||||
gonum.org/v1/gonum v0.17.0
|
||||
google.golang.org/api v0.265.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/apimachinery v0.35.2
|
||||
|
||||
@@ -64,6 +64,97 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}", handler.New(provider.authZ.EditAccess(provider.dashboardHandler.PatchV2), handler.OpenAPIDef{
|
||||
ID: "PatchDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Patch dashboard (v2)",
|
||||
Description: "This endpoint applies an RFC 6902 JSON Patch to a v2-shape dashboard. The patch is applied against the postable view of the dashboard (metadata, data, tags), so individual panels, queries, variables, layouts, or tags can be updated without re-sending the rest of the dashboard. Locked dashboards are rejected.",
|
||||
Request: new(dashboardtypes.JSONPatchDocument),
|
||||
// Strictly per RFC 6902 the content type is `application/json-patch+json`,
|
||||
// but our OpenAPI generator only reflects schemas for content types it
|
||||
// understands (application/json, form-urlencoded, multipart) — anything
|
||||
// else degrades to `type: string`. Declaring application/json here keeps
|
||||
// the array-of-ops schema visible to spec consumers; the runtime decoder
|
||||
// parses JSON regardless of the request's actual Content-Type header.
|
||||
RequestContentType: "application/json",
|
||||
Response: new(dashboardtypes.GettableDashboardV2),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}/lock", handler.New(provider.authZ.EditAccess(provider.dashboardHandler.LockV2), handler.OpenAPIDef{
|
||||
ID: "LockDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Lock dashboard (v2)",
|
||||
Description: "This endpoint locks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}/lock", handler.New(provider.authZ.EditAccess(provider.dashboardHandler.UnlockV2), handler.OpenAPIDef{
|
||||
ID: "UnlockDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Unlock dashboard (v2)",
|
||||
Description: "This endpoint unlocks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}/public", handler.New(provider.authZ.AdminAccess(provider.dashboardHandler.CreatePublicV2), handler.OpenAPIDef{
|
||||
ID: "CreatePublicDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Make a dashboard v2 public",
|
||||
Description: "This endpoint creates the public sharing config for a v2 dashboard and returns the dashboard with the new public config attached. Lock state does not gate this endpoint.",
|
||||
Request: new(dashboardtypes.PostablePublicDashboard),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(dashboardtypes.GettableDashboardV2),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}/public", handler.New(provider.authZ.AdminAccess(provider.dashboardHandler.UpdatePublicV2), handler.OpenAPIDef{
|
||||
ID: "UpdatePublicDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Update public sharing config for a dashboard v2",
|
||||
Description: "This endpoint updates the public sharing config (time range settings) of an already-public v2 dashboard. Lock state does not gate this endpoint.",
|
||||
Request: new(dashboardtypes.UpdatablePublicDashboard),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(dashboardtypes.GettableDashboardV2),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authZ.AdminAccess(provider.dashboardHandler.CreatePublic), handler.OpenAPIDef{
|
||||
ID: "CreatePublicDashboard",
|
||||
Tags: []string{"dashboard"},
|
||||
|
||||
@@ -62,6 +62,14 @@ type Module interface {
|
||||
GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error)
|
||||
|
||||
UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updateable dashboardtypes.UpdateableDashboardV2) (*dashboardtypes.DashboardV2, error)
|
||||
|
||||
PatchV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, patch dashboardtypes.PatchableDashboardV2) (*dashboardtypes.DashboardV2, error)
|
||||
|
||||
LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error
|
||||
|
||||
CreatePublicV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, postable dashboardtypes.PostablePublicDashboard) (*dashboardtypes.DashboardV2, error)
|
||||
|
||||
UpdatePublicV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatable dashboardtypes.UpdatablePublicDashboard) (*dashboardtypes.DashboardV2, error)
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
@@ -93,4 +101,14 @@ type Handler interface {
|
||||
GetV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
UpdateV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
PatchV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
LockV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
UnlockV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
CreatePublicV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
UpdatePublicV2(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
@@ -152,6 +152,31 @@ func (store *store) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.U
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, locked bool, updatedBy string) error {
|
||||
res, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewUpdate().
|
||||
Model((*dashboardtypes.StorableDashboard)(nil)).
|
||||
Set("locked = ?", locked).
|
||||
Set("updated_by = ?", updatedBy).
|
||||
Set("updated_at = ?", time.Now()).
|
||||
Where("id = ?", id).
|
||||
Where("deleted_at IS NULL").
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rows == 0 {
|
||||
return errors.Newf(errors.TypeNotFound, dashboardtypes.ErrCodeDashboardNotFound, "dashboard with id %s doesn't exist", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) GetPublic(ctx context.Context, dashboardID string) (*dashboardtypes.StorablePublicDashboard, error) {
|
||||
storable := new(dashboardtypes.StorablePublicDashboard)
|
||||
err := store.
|
||||
|
||||
@@ -81,6 +81,65 @@ func (handler *handler) GetV2(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Success(rw, http.StatusOK, dashboardtypes.NewGettableDashboardV2FromDashboardV2(dashboard))
|
||||
}
|
||||
|
||||
func (handler *handler) LockV2(rw http.ResponseWriter, r *http.Request) {
|
||||
handler.lockUnlockV2(rw, r, true)
|
||||
}
|
||||
|
||||
func (handler *handler) UnlockV2(rw http.ResponseWriter, r *http.Request) {
|
||||
handler.lockUnlockV2(rw, r, false)
|
||||
}
|
||||
|
||||
func (handler *handler) lockUnlockV2(rw http.ResponseWriter, r *http.Request, lock bool) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
isAdmin := false
|
||||
selectors := []authtypes.Selector{
|
||||
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
|
||||
}
|
||||
if err := handler.authz.CheckWithTupleCreation(
|
||||
ctx,
|
||||
claims,
|
||||
valuer.MustNewUUID(claims.OrgID),
|
||||
authtypes.RelationAssignee,
|
||||
authtypes.TypeableRole,
|
||||
selectors,
|
||||
selectors,
|
||||
); err == nil {
|
||||
isAdmin = true
|
||||
}
|
||||
|
||||
if err := handler.module.LockUnlockV2(ctx, orgID, dashboardID, claims.Email, isAdmin, lock); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) UpdateV2(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
@@ -122,3 +181,129 @@ func (handler *handler) UpdateV2(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboardtypes.NewGettableDashboardV2FromDashboardV2(dashboard))
|
||||
}
|
||||
|
||||
func (handler *handler) PatchV2(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
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := dashboardtypes.PatchableDashboardV2{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.PatchV2(ctx, orgID, dashboardID, claims.Email, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboardtypes.NewGettableDashboardV2FromDashboardV2(dashboard))
|
||||
}
|
||||
|
||||
func (handler *handler) CreatePublicV2(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
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := dashboardtypes.PostablePublicDashboard{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.CreatePublicV2(ctx, orgID, dashboardID, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboardtypes.NewGettableDashboardV2FromDashboardV2(dashboard))
|
||||
}
|
||||
|
||||
func (handler *handler) UpdatePublicV2(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
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := dashboardtypes.UpdatablePublicDashboard{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.UpdatePublicV2(ctx, orgID, dashboardID, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboardtypes.NewGettableDashboardV2FromDashboardV2(dashboard))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package impldashboard
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
@@ -109,3 +110,69 @@ func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer
|
||||
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
func (module *module) PatchV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, patch dashboardtypes.PatchableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
existing, err := module.GetV2(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := existing.CanUpdate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateable, err := patch.Apply(existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resolvedTags, err := module.tagModule.CreateMany(ctx, orgID, updateable.Tags, updatedBy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tagIDs := make([]valuer.UUID, len(resolvedTags))
|
||||
for i, t := range resolvedTags {
|
||||
tagIDs[i] = t.ID
|
||||
}
|
||||
|
||||
if err := existing.Update(*updateable, updatedBy, resolvedTags); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
storable, err := existing.ToStorableDashboard()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = module.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
|
||||
if err := module.tagModule.SyncLinksForEntity(ctx, orgID, dashboardtypes.EntityTypeDashboard, id, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
return module.store.UpdateV2(ctx, orgID, id, updatedBy, storable.Data)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
// CreatePublicV2 is not supported in the community build.
|
||||
func (module *module) CreatePublicV2(_ context.Context, _ valuer.UUID, _ valuer.UUID, _ dashboardtypes.PostablePublicDashboard) (*dashboardtypes.DashboardV2, error) {
|
||||
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
|
||||
}
|
||||
|
||||
// UpdatePublicV2 is not supported in the community build.
|
||||
func (module *module) UpdatePublicV2(_ context.Context, _ valuer.UUID, _ valuer.UUID, _ dashboardtypes.UpdatablePublicDashboard) (*dashboardtypes.DashboardV2, error) {
|
||||
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
|
||||
}
|
||||
|
||||
func (module *module) LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
|
||||
existing, err := module.GetV2(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := existing.LockUnlock(lock, isAdmin, updatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
return module.store.LockUnlockV2(ctx, orgID, id, lock, updatedBy)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
jsonpatch "gopkg.in/evanphx/json-patch.v4"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -52,6 +54,88 @@ type PostableDashboardV2 struct {
|
||||
|
||||
type UpdateableDashboardV2 = PostableDashboardV2
|
||||
|
||||
// PatchableDashboardV2 is an RFC 6902 JSON Patch document applied against a
|
||||
// PostableDashboardV2-shaped view of an existing dashboard. Patch ops can
|
||||
// target any field — including individual entries inside `data.panels`,
|
||||
// `data.panels.<id>.spec.queries`, or `tags` — without re-sending the rest of
|
||||
// the dashboard.
|
||||
type PatchableDashboardV2 struct {
|
||||
patch jsonpatch.Patch
|
||||
}
|
||||
|
||||
// JSONPatchDocument is the OpenAPI-facing schema for an RFC 6902 patch body.
|
||||
// PatchableDashboardV2 has only an internal `jsonpatch.Patch` field, so the
|
||||
// reflector would emit an empty schema; the handler def points at this type
|
||||
// instead so consumers see the array-of-ops shape.
|
||||
type JSONPatchDocument []JSONPatchOperation
|
||||
|
||||
// JSONPatchOperation is one RFC 6902 op. Not every field is valid on every
|
||||
// op kind (e.g. `value` is required for add/replace/test, ignored for remove;
|
||||
// `from` is required for move/copy) — the JSON Patch RFC governs that.
|
||||
type JSONPatchOperation struct {
|
||||
Op string `json:"op" required:"true"`
|
||||
Path string `json:"path" required:"true" description:"JSON Pointer (RFC 6901) into the dashboard's postable shape — e.g. /data/display/name, /data/panels/<id>, /data/panels/<id>/spec/queries/0, /tags/-."`
|
||||
Value any `json:"value,omitempty" description:"Value to add/replace/test against. The expected type depends on the path. Common shapes (see referenced schemas for the exact field set): /data/panels/<id> takes a DashboardtypesPanel; /data/panels/<id>/spec/queries/N (or /-) takes a DashboardtypesQuery; /data/variables/N takes a DashboardtypesVariable; /data/layouts/N takes a DashboardtypesLayout; /tags/N (or /-) takes a TagtypesPostableTag; /data/display/name and other leaf string fields take a string. Required for add/replace/test; ignored for remove/move/copy."`
|
||||
From string `json:"from,omitempty" description:"Source JSON Pointer for move/copy ops; ignored for other ops."`
|
||||
}
|
||||
|
||||
// PrepareJSONSchema constrains the `op` field to the six RFC 6902 verbs.
|
||||
func (JSONPatchOperation) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
op, ok := s.Properties["op"]
|
||||
if !ok || op.TypeObject == nil {
|
||||
return errors.NewInternalf(errors.CodeInternal, "JSONPatchOperation schema missing `op` property")
|
||||
}
|
||||
op.TypeObject.WithEnum("add", "remove", "replace", "move", "copy", "test")
|
||||
s.Properties["op"] = op
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchableDashboardV2) UnmarshalJSON(data []byte) error {
|
||||
patch, err := jsonpatch.DecodePatch(data)
|
||||
if err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s", err.Error())
|
||||
}
|
||||
p.patch = patch
|
||||
return nil
|
||||
}
|
||||
|
||||
// patchableDashboardV2View is the JSON shape a patch is applied against.
|
||||
// It mirrors PostableDashboardV2 except `tags` is always emitted (even when
|
||||
// empty) — RFC 6902 `add /tags/-` requires the array to exist in the target
|
||||
// document, and PostableDashboardV2's own `omitempty` on tags would drop it.
|
||||
type patchableDashboardV2View struct {
|
||||
StoredDashboardInfo
|
||||
Tags []tagtypes.PostableTag `json:"tags"`
|
||||
}
|
||||
|
||||
// Apply runs the patch against the existing dashboard. The dashboard is
|
||||
// projected into the postable JSON shape, the patch is applied, and the
|
||||
// result is decoded back into an UpdateableDashboardV2 — which re-runs
|
||||
// the full v2 validation chain.
|
||||
func (p PatchableDashboardV2) Apply(existing *DashboardV2) (*UpdateableDashboardV2, error) {
|
||||
postableTags := make([]tagtypes.PostableTag, len(existing.Info.Tags))
|
||||
for i, t := range existing.Info.Tags {
|
||||
postableTags[i] = tagtypes.PostableTag{Name: t.Name}
|
||||
}
|
||||
base := patchableDashboardV2View{
|
||||
StoredDashboardInfo: existing.Info.StoredDashboardInfo,
|
||||
Tags: postableTags,
|
||||
}
|
||||
raw, err := json.Marshal(base)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "marshal existing dashboard for patch")
|
||||
}
|
||||
patched, err := p.patch.Apply(raw)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s", err.Error())
|
||||
}
|
||||
out := &UpdateableDashboardV2{}
|
||||
if err := json.Unmarshal(patched, out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (p *PostableDashboardV2) UnmarshalJSON(data []byte) error {
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
@@ -165,6 +249,29 @@ func NewDashboardV2FromStorable(storable *StorableDashboard, public *StorablePub
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DashboardV2) CanLockUnlock(lock bool, isAdmin bool, updatedBy string) error {
|
||||
if d.CreatedBy != updatedBy && !isAdmin {
|
||||
return errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "you are not authorized to lock/unlock this dashboard")
|
||||
}
|
||||
if d.Locked == lock {
|
||||
if lock {
|
||||
return errors.Newf(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "dashboard is already locked")
|
||||
}
|
||||
return errors.Newf(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "dashboard is already unlocked")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DashboardV2) LockUnlock(lock bool, isAdmin bool, updatedBy string) error {
|
||||
if err := d.CanLockUnlock(lock, isAdmin, updatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Locked = lock
|
||||
d.UpdatedBy = updatedBy
|
||||
d.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DashboardV2) CanUpdate() error {
|
||||
if d.Locked {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cannot update a locked dashboard, please unlock the dashboard to update")
|
||||
|
||||
521
pkg/types/dashboardtypes/perses_dashboard_patch_test.go
Normal file
521
pkg/types/dashboardtypes/perses_dashboard_patch_test.go
Normal file
@@ -0,0 +1,521 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// basePostableJSON is the postable shape of a small but realistic v2
|
||||
// dashboard used as the base document for patch tests. Each panel carries
|
||||
// one builder query in the same shape production dashboards use
|
||||
// (aggregations, filter, groupBy populated), and the dashboard has one
|
||||
// variable — the variable is not patched in any test here, that's
|
||||
// covered in a separate variable-focused suite.
|
||||
const basePostableJSON = `{
|
||||
"metadata": {"schemaVersion": "v6"},
|
||||
"data": {
|
||||
"display": {"name": "Service overview"},
|
||||
"variables": [
|
||||
{
|
||||
"kind": "ListVariable",
|
||||
"spec": {
|
||||
"name": "service",
|
||||
"allowAllValue": true,
|
||||
"allowMultiple": false,
|
||||
"plugin": {
|
||||
"kind": "signoz/DynamicVariable",
|
||||
"spec": {"name": "service.name", "signal": "metrics"}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"panels": {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [{
|
||||
"metricName": "signoz_calls_total",
|
||||
"temporality": "cumulative",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum"
|
||||
}],
|
||||
"filter": {"expression": "service.name IN $service"},
|
||||
"groupBy": [{"name": "service.name", "fieldDataType": "string", "fieldContext": "tag"}]
|
||||
}}}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"p2": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"plugin": {"kind": "signoz/NumberPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
"name": "X",
|
||||
"signal": "metrics",
|
||||
"aggregations": [{
|
||||
"metricName": "signoz_latency_count",
|
||||
"temporality": "cumulative",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum"
|
||||
}]
|
||||
}}}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"layouts": [
|
||||
{
|
||||
"kind": "Grid",
|
||||
"spec": {
|
||||
"display": {"title": "Row 1"},
|
||||
"items": [
|
||||
{"x": 0, "y": 0, "width": 6, "height": 6, "content": {"$ref": "#/spec/panels/p1"}},
|
||||
{"x": 6, "y": 0, "width": 6, "height": 6, "content": {"$ref": "#/spec/panels/p2"}}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"duration": "1h"
|
||||
},
|
||||
"tags": [{"name": "team/alpha"}, {"name": "env/prod"}]
|
||||
}`
|
||||
|
||||
func TestPatchableDashboardV2_Apply(t *testing.T) {
|
||||
// Apply doesn't mutate the input *DashboardV2 — it marshals it to
|
||||
// JSON, applies the patch, and unmarshals the result into a fresh
|
||||
// struct. Sharing one base across subtests is safe.
|
||||
var p PostableDashboardV2
|
||||
require.NoError(t, json.Unmarshal([]byte(basePostableJSON), &p), "base postable JSON must validate")
|
||||
base := &DashboardV2{
|
||||
Info: DashboardInfo{
|
||||
StoredDashboardInfo: p.StoredDashboardInfo,
|
||||
Tags: []*tagtypes.Tag{
|
||||
{Name: "team/alpha", InternalName: "team::alpha"},
|
||||
{Name: "env/prod", InternalName: "env::prod"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
decode := func(t *testing.T, body string) PatchableDashboardV2 {
|
||||
t.Helper()
|
||||
var patch PatchableDashboardV2
|
||||
require.NoError(t, json.Unmarshal([]byte(body), &patch))
|
||||
return patch
|
||||
}
|
||||
|
||||
// jsonOf marshals the patched dashboard back to JSON so subtests can
|
||||
// assert on field values without reaching into the typed plugin specs.
|
||||
jsonOf := func(t *testing.T, out *UpdateableDashboardV2) string {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(out)
|
||||
require.NoError(t, err)
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Successful patches
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
t.Run("no-op preserves all fields", func(t *testing.T) {
|
||||
out, err := decode(t, `[]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, base.Info.Metadata, out.Metadata)
|
||||
assert.Equal(t, base.Info.Data.Display.Name, out.Data.Display.Name)
|
||||
require.Equal(t, len(base.Info.Data.Panels), len(out.Data.Panels))
|
||||
for k, panel := range base.Info.Data.Panels {
|
||||
require.Contains(t, out.Data.Panels, k)
|
||||
assert.Equal(t, panel.Spec.Plugin.Kind, out.Data.Panels[k].Spec.Plugin.Kind)
|
||||
}
|
||||
assert.Len(t, out.Tags, len(base.Info.Tags))
|
||||
assert.Len(t, out.Data.Variables, len(base.Info.Data.Variables))
|
||||
assert.Len(t, out.Data.Layouts, len(base.Info.Data.Layouts))
|
||||
})
|
||||
|
||||
t.Run("add metadata image", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "add", "path": "/metadata/image", "value": "https://example.com/img.png"}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://example.com/img.png", out.Metadata.Image)
|
||||
assert.Equal(t, SchemaVersion, out.Metadata.SchemaVersion, "schemaVersion preserved")
|
||||
})
|
||||
|
||||
t.Run("replace display name", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "replace", "path": "/data/display/name", "value": "Renamed"}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Renamed", out.Data.Display.Name)
|
||||
})
|
||||
|
||||
// Per RFC 6902 § 4.1, `add` on an existing object member replaces the
|
||||
// existing value rather than erroring — same effect as `replace`.
|
||||
t.Run("add overwrites existing display name", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "add", "path": "/data/display/name", "value": "Overwritten"}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Overwritten", out.Data.Display.Name)
|
||||
})
|
||||
|
||||
t.Run("add data refreshInterval", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "add", "path": "/data/refreshInterval", "value": "30s"}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "30s", string(out.Data.RefreshInterval))
|
||||
})
|
||||
|
||||
t.Run("add panel leaves others untouched", func(t *testing.T) {
|
||||
out, err := decode(t, `[{
|
||||
"op": "add",
|
||||
"path": "/data/panels/p3",
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"plugin": {"kind": "signoz/TablePanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"aggregations": [{"expression": "count()"}]
|
||||
}}}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, out.Data.Panels, 3)
|
||||
assert.Contains(t, out.Data.Panels, "p1")
|
||||
assert.Contains(t, out.Data.Panels, "p2")
|
||||
assert.Contains(t, out.Data.Panels, "p3")
|
||||
})
|
||||
|
||||
t.Run("replace single panel", func(t *testing.T) {
|
||||
out, err := decode(t, `[{
|
||||
"op": "replace",
|
||||
"path": "/data/panels/p2",
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"plugin": {"kind": "signoz/BarChartPanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [{
|
||||
"metricName": "signoz_calls_total",
|
||||
"temporality": "cumulative",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum"
|
||||
}]
|
||||
}}}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, PanelPluginKind("signoz/BarChartPanel"), out.Data.Panels["p2"].Spec.Plugin.Kind)
|
||||
assert.Equal(t, PanelPluginKind("signoz/TimeSeriesPanel"), out.Data.Panels["p1"].Spec.Plugin.Kind, "p1 untouched")
|
||||
})
|
||||
|
||||
// Removing a panel realistically also drops its layout item — exercise
|
||||
// the multi-op shape the UI sends.
|
||||
t.Run("remove panel and its layout item", func(t *testing.T) {
|
||||
out, err := decode(t, `[
|
||||
{"op": "remove", "path": "/data/panels/p2"},
|
||||
{"op": "remove", "path": "/data/layouts/0/spec/items/1"}
|
||||
]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, out.Data.Panels, 1)
|
||||
assert.Contains(t, out.Data.Panels, "p1")
|
||||
assert.NotContains(t, out.Data.Panels, "p2")
|
||||
raw := jsonOf(t, out)
|
||||
assert.NotContains(t, raw, `"$ref":"#/spec/panels/p2"`)
|
||||
assert.Contains(t, raw, `"$ref":"#/spec/panels/p1"`)
|
||||
})
|
||||
|
||||
// The headline use case: edit a single field of a single query inside
|
||||
// one panel without re-sending any other part of the dashboard.
|
||||
t.Run("rename single query inside panel", func(t *testing.T) {
|
||||
out, err := decode(t, `[{
|
||||
"op": "replace",
|
||||
"path": "/data/panels/p1/spec/queries/0/spec/plugin/spec/name",
|
||||
"value": "renamed"
|
||||
}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, out.Data.Panels["p1"].Spec.Queries, 1)
|
||||
assert.Contains(t, jsonOf(t, out), `"name":"renamed"`)
|
||||
})
|
||||
|
||||
// Replace a query at a specific index — swaps query "A" out for "B"
|
||||
// without re-sending the rest of the panel.
|
||||
t.Run("replace query at index", func(t *testing.T) {
|
||||
out, err := decode(t, `[{
|
||||
"op": "replace",
|
||||
"path": "/data/panels/p1/spec/queries/0",
|
||||
"value": {
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
"name": "B",
|
||||
"signal": "metrics",
|
||||
"aggregations": [{
|
||||
"metricName": "signoz_db_calls_total",
|
||||
"temporality": "cumulative",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum"
|
||||
}]
|
||||
}}}
|
||||
}
|
||||
}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.Data.Panels["p1"].Spec.Queries, 1)
|
||||
raw := jsonOf(t, out)
|
||||
assert.Contains(t, raw, `"name":"B"`)
|
||||
assert.NotContains(t, raw, `"name":"A"`)
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Layout edits
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
t.Run("move panel by editing layout x coordinate", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "replace", "path": "/data/layouts/0/spec/items/0/x", "value": 6}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
raw := jsonOf(t, out)
|
||||
// The first item used to live at x=0, now lives at x=6.
|
||||
assert.Contains(t, raw, `"x":6,"y":0,"width":6,"height":6,"content":{"$ref":"#/spec/panels/p1"}`)
|
||||
})
|
||||
|
||||
t.Run("resize panel by editing layout width", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "replace", "path": "/data/layouts/0/spec/items/0/width", "value": 12}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
raw := jsonOf(t, out)
|
||||
assert.Contains(t, raw, `"width":12`)
|
||||
})
|
||||
|
||||
t.Run("rename layout row title", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "replace", "path": "/data/layouts/0/spec/display/title", "value": "Latency"}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, jsonOf(t, out), `"title":"Latency"`)
|
||||
})
|
||||
|
||||
t.Run("append layout item", func(t *testing.T) {
|
||||
out, err := decode(t, `[{
|
||||
"op": "add",
|
||||
"path": "/data/layouts/0/spec/items/-",
|
||||
"value": {"x": 0, "y": 6, "width": 12, "height": 6, "content": {"$ref": "#/spec/panels/p1"}}
|
||||
}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
// Item count went 2 → 3.
|
||||
raw := jsonOf(t, out)
|
||||
assert.Equal(t, 3, strings.Count(raw, `"$ref":"#/spec/panels/`))
|
||||
})
|
||||
|
||||
// Composing add-panel + add-layout-item is the realistic shape of the
|
||||
// "add a new chart to my dashboard" UI flow — exercise it end-to-end.
|
||||
t.Run("add panel and corresponding layout item", func(t *testing.T) {
|
||||
out, err := decode(t, `[
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/data/panels/p3",
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"plugin": {"kind": "signoz/TablePanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"aggregations": [{"expression": "count()"}]
|
||||
}}}
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/data/layouts/0/spec/items/-",
|
||||
"value": {"x": 0, "y": 6, "width": 12, "height": 6, "content": {"$ref": "#/spec/panels/p3"}}
|
||||
}
|
||||
]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, out.Data.Panels, 3)
|
||||
raw := jsonOf(t, out)
|
||||
assert.Contains(t, raw, `"$ref":"#/spec/panels/p3"`)
|
||||
})
|
||||
|
||||
t.Run("append tag", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "add", "path": "/tags/-", "value": {"name": "env/staging"}}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.Tags, 3)
|
||||
assert.Equal(t, "env/staging", out.Tags[2].Name)
|
||||
})
|
||||
|
||||
t.Run("append tag when none exist", func(t *testing.T) {
|
||||
noTagsBase := &DashboardV2{
|
||||
Info: DashboardInfo{
|
||||
StoredDashboardInfo: base.Info.StoredDashboardInfo,
|
||||
Tags: nil,
|
||||
},
|
||||
}
|
||||
out, err := decode(t, `[{"op": "add", "path": "/tags/-", "value": {"name": "team/new"}}]`).Apply(noTagsBase)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.Tags, 1)
|
||||
assert.Equal(t, "team/new", out.Tags[0].Name)
|
||||
})
|
||||
|
||||
t.Run("replace tag name", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "replace", "path": "/tags/0/name", "value": "team/beta"}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.Tags, 2)
|
||||
assert.Equal(t, "team/beta", out.Tags[0].Name)
|
||||
assert.Equal(t, "env/prod", out.Tags[1].Name, "tag at index 1 untouched")
|
||||
for _, tag := range out.Tags {
|
||||
assert.NotEqual(t, "team/alpha", tag.Name, "old tag name must be gone")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple ops applied in order", func(t *testing.T) {
|
||||
out, err := decode(t, `[
|
||||
{"op": "replace", "path": "/data/display/name", "value": "Multi-step"},
|
||||
{"op": "remove", "path": "/data/panels/p2"},
|
||||
{"op": "add", "path": "/tags/-", "value": {"name": "env/staging"}}
|
||||
]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Multi-step", out.Data.Display.Name)
|
||||
assert.Len(t, out.Data.Panels, 1)
|
||||
assert.Len(t, out.Tags, 3)
|
||||
})
|
||||
|
||||
// `test` is an RFC 6902 precondition op: aborts the patch if the value
|
||||
// at the path doesn't equal the supplied value. Used for optimistic
|
||||
// concurrency. Here it matches, so the subsequent ops apply.
|
||||
t.Run("test op passes", func(t *testing.T) {
|
||||
out, err := decode(t, `[
|
||||
{"op": "test", "path": "/data/display/name", "value": "Service overview"},
|
||||
{"op": "replace", "path": "/data/display/name", "value": "Confirmed"}
|
||||
]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Confirmed", out.Data.Display.Name)
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Failure cases
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
t.Run("decode rejects non-array body", func(t *testing.T) {
|
||||
var patch PatchableDashboardV2
|
||||
err := json.Unmarshal([]byte(`{"op": "replace"}`), &patch)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("decode rejects malformed JSON", func(t *testing.T) {
|
||||
var patch PatchableDashboardV2
|
||||
// Outer json.Unmarshal rejects non-JSON before PatchableDashboardV2's
|
||||
// UnmarshalJSON runs, so the error is a stdlib SyntaxError rather
|
||||
// than the InvalidInput-classified wrap.
|
||||
err := json.Unmarshal([]byte(`not json`), &patch)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
// `test` precondition fails — the whole patch is rejected, including
|
||||
// the subsequent replace.
|
||||
t.Run("test op failure rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[
|
||||
{"op": "test", "path": "/data/display/name", "value": "Wrong"},
|
||||
{"op": "replace", "path": "/data/display/name", "value": "Should not apply"}
|
||||
]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("remove at missing path rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[{"op": "remove", "path": "/data/panels/does-not-exist"}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("remove schemaVersion rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[{"op": "remove", "path": "/metadata/schemaVersion"}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("wrong schemaVersion rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[{"op": "replace", "path": "/metadata/schemaVersion", "value": "v5"}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), SchemaVersion)
|
||||
})
|
||||
|
||||
t.Run("empty display name rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[{"op": "replace", "path": "/data/display/name", "value": ""}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "data.display.name is required")
|
||||
})
|
||||
|
||||
t.Run("unknown top-level field rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[{"op": "add", "path": "/bogus", "value": 42}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "bogus")
|
||||
})
|
||||
|
||||
t.Run("invalid panel kind rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[{
|
||||
"op": "replace",
|
||||
"path": "/data/panels/p1",
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {"plugin": {"kind": "signoz/NotAPanel", "spec": {}}}
|
||||
}
|
||||
}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "NotAPanel")
|
||||
})
|
||||
|
||||
t.Run("query kind incompatible with panel rejected", func(t *testing.T) {
|
||||
// PromQLQuery is not allowed on ListPanel — verify the cross-check
|
||||
// in Validate still runs after a patch.
|
||||
_, err := decode(t, `[{
|
||||
"op": "replace",
|
||||
"path": "/data/panels/p2",
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"plugin": {"kind": "signoz/ListPanel", "spec": {}},
|
||||
"queries": [{"kind": "TimeSeriesQuery", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
|
||||
}
|
||||
}
|
||||
}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("removing only query rejected", func(t *testing.T) {
|
||||
// p2 has exactly one query in the base; removing it would strand
|
||||
// the panel queryless, which Validate now rejects.
|
||||
_, err := decode(t, `[{"op": "remove", "path": "/data/panels/p2/spec/queries/0"}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "at least one query")
|
||||
})
|
||||
|
||||
t.Run("too many tags rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[
|
||||
{"op": "add", "path": "/tags/-", "value": {"name": "t1"}},
|
||||
{"op": "add", "path": "/tags/-", "value": {"name": "t2"}},
|
||||
{"op": "add", "path": "/tags/-", "value": {"name": "t3"}},
|
||||
{"op": "add", "path": "/tags/-", "value": {"name": "t4"}}
|
||||
]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "at most")
|
||||
})
|
||||
}
|
||||
@@ -42,4 +42,6 @@ type Store interface {
|
||||
// only, scoped by org and excluding soft-deleted rows. Uses the caller's
|
||||
// transaction context so it can be made atomic with tag relation changes.
|
||||
UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, data StorableDashboardData) error
|
||||
|
||||
LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, locked bool, updatedBy string) error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user