Compare commits

..

5 Commits

Author SHA1 Message Date
SagarRajput-7
81e9482aaa Merge branch 'main' into sentry-fixes 2026-06-11 18:42:57 +05:30
SagarRajput-7
ed111e10d9 fix(settings): test fixes 2026-06-11 17:16:55 +05:30
SagarRajput-7
bfb8334737 Merge branch 'main' into sentry-fixes 2026-06-11 16:59:16 +05:30
SagarRajput-7
fd2cc3145b fix(settings): guard against non-APIError in logs retention error state 2026-06-11 16:53:41 +05:30
SagarRajput-7
cbef40d209 fix(settings): disable RouteTab tab panel animation to prevent insertBefore crash 2026-06-11 16:37:28 +05:30
20 changed files with 80 additions and 1525 deletions

View File

@@ -2590,41 +2590,6 @@ components:
- panels
- layouts
type: object
DashboardtypesDashboardView:
properties:
createdAt:
format: date-time
type: string
data:
$ref: '#/components/schemas/DashboardtypesDashboardViewData'
id:
type: string
name:
type: string
orgId:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- data
- orgId
type: object
DashboardtypesDashboardViewData:
properties:
order:
$ref: '#/components/schemas/DashboardtypesListOrder'
query:
type: string
sort:
$ref: '#/components/schemas/DashboardtypesListSort'
version:
type: string
required:
- version
type: object
DashboardtypesDatasourcePlugin:
discriminator:
mapping:
@@ -2909,15 +2874,6 @@ components:
- total
- tags
type: object
DashboardtypesListableDashboardView:
properties:
views:
items:
$ref: '#/components/schemas/DashboardtypesDashboardView'
type: array
required:
- views
type: object
DashboardtypesListedDashboardForUserV2:
properties:
createdAt:
@@ -3222,16 +3178,6 @@ components:
- tags
- spec
type: object
DashboardtypesPostableDashboardView:
properties:
data:
$ref: '#/components/schemas/DashboardtypesDashboardViewData'
name:
type: string
required:
- name
- data
type: object
DashboardtypesPostablePublicDashboard:
properties:
defaultTimeRange:
@@ -13382,235 +13328,6 @@ paths:
summary: Update user preference
tags:
- preferences
/api/v2/dashboard_views:
get:
deprecated: false
description: Returns every saved view in the calling user's org. Saved views
are shared org-wide.
operationId: ListDashboardViews
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesListableDashboardView'
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:
- VIEWER
- tokenizer:
- VIEWER
summary: List dashboard saved views
tags:
- dashboard
post:
deprecated: false
description: Persists the calling user's dashboard listing state (query, sort,
order) as a named, reusable view shared across the org.
operationId: CreateDashboardView
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/DashboardtypesPostableDashboardView'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesDashboardView'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- EDITOR
- tokenizer:
- EDITOR
summary: Create dashboard saved view
tags:
- dashboard
/api/v2/dashboard_views/{id}:
delete:
deprecated: false
description: Removes a saved view. Saved views are shared org-wide. Deleting
a non-existent view returns 404.
operationId: DeleteDashboardView
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- EDITOR
- tokenizer:
- EDITOR
summary: Delete dashboard saved view
tags:
- dashboard
put:
deprecated: false
description: Replaces a saved view's name and data. Saved views are shared org-wide.
operationId: UpdateDashboardView
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/DashboardtypesPostableDashboardView'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesDashboardView'
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:
- EDITOR
- tokenizer:
- EDITOR
summary: Update dashboard saved view
tags:
- dashboard
/api/v2/dashboards:
get:
deprecated: false

View File

@@ -262,22 +262,6 @@ func (module *module) DeletePreferencesForUser(ctx context.Context, orgID valuer
return module.pkgDashboardModule.DeletePreferencesForUser(ctx, orgID, userID)
}
func (module *module) CreateView(ctx context.Context, orgID valuer.UUID, postable dashboardtypes.PostableDashboardView) (*dashboardtypes.DashboardView, error) {
return module.pkgDashboardModule.CreateView(ctx, orgID, postable)
}
func (module *module) ListViews(ctx context.Context, orgID valuer.UUID) (*dashboardtypes.ListableDashboardView, error) {
return module.pkgDashboardModule.ListViews(ctx, orgID)
}
func (module *module) UpdateView(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updateable dashboardtypes.UpdatableDashboardView) (*dashboardtypes.DashboardView, error) {
return module.pkgDashboardModule.UpdateView(ctx, orgID, id, updateable)
}
func (module *module) DeleteView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
return module.pkgDashboardModule.DeleteView(ctx, orgID, id)
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Get(ctx, orgID, id)
}

View File

@@ -19,17 +19,14 @@ import type {
import type {
CreateDashboardV2201,
CreateDashboardView201,
CreatePublicDashboard201,
CreatePublicDashboardPathParameters,
DashboardtypesPatchableDashboardV2DTO,
DashboardtypesPostableDashboardV2DTO,
DashboardtypesPostableDashboardViewDTO,
DashboardtypesPostablePublicDashboardDTO,
DashboardtypesUpdatableDashboardV2DTO,
DashboardtypesUpdatablePublicDashboardDTO,
DeleteDashboardV2PathParameters,
DeleteDashboardViewPathParameters,
DeletePublicDashboardPathParameters,
GetDashboardV2200,
GetDashboardV2PathParameters,
@@ -39,7 +36,6 @@ import type {
GetPublicDashboardPathParameters,
GetPublicDashboardWidgetQueryRange200,
GetPublicDashboardWidgetQueryRangePathParameters,
ListDashboardViews200,
ListDashboardsForUserV2200,
ListDashboardsForUserV2Params,
ListDashboardsV2200,
@@ -53,8 +49,6 @@ import type {
UnpinDashboardV2PathParameters,
UpdateDashboardV2200,
UpdateDashboardV2PathParameters,
UpdateDashboardView200,
UpdateDashboardViewPathParameters,
UpdatePublicDashboardPathParameters,
} from '../sigNoz.schemas';
@@ -654,354 +648,6 @@ export const invalidateGetPublicDashboardWidgetQueryRange = async (
return queryClient;
};
/**
* Returns every saved view in the calling user's org. Saved views are shared org-wide.
* @summary List dashboard saved views
*/
export const listDashboardViews = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListDashboardViews200>({
url: `/api/v2/dashboard_views`,
method: 'GET',
signal,
});
};
export const getListDashboardViewsQueryKey = () => {
return [`/api/v2/dashboard_views`] as const;
};
export const getListDashboardViewsQueryOptions = <
TData = Awaited<ReturnType<typeof listDashboardViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listDashboardViews>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListDashboardViewsQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listDashboardViews>>
> = ({ signal }) => listDashboardViews(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listDashboardViews>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListDashboardViewsQueryResult = NonNullable<
Awaited<ReturnType<typeof listDashboardViews>>
>;
export type ListDashboardViewsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List dashboard saved views
*/
export function useListDashboardViews<
TData = Awaited<ReturnType<typeof listDashboardViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listDashboardViews>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListDashboardViewsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List dashboard saved views
*/
export const invalidateListDashboardViews = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListDashboardViewsQueryKey() },
options,
);
return queryClient;
};
/**
* Persists the calling user's dashboard listing state (query, sort, order) as a named, reusable view shared across the org.
* @summary Create dashboard saved view
*/
export const createDashboardView = (
dashboardtypesPostableDashboardViewDTO?: BodyType<DashboardtypesPostableDashboardViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateDashboardView201>({
url: `/api/v2/dashboard_views`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: dashboardtypesPostableDashboardViewDTO,
signal,
});
};
export const getCreateDashboardViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDashboardView>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createDashboardView>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
TContext
> => {
const mutationKey = ['createDashboardView'];
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 createDashboardView>>,
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> }
> = (props) => {
const { data } = props ?? {};
return createDashboardView(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateDashboardViewMutationResult = NonNullable<
Awaited<ReturnType<typeof createDashboardView>>
>;
export type CreateDashboardViewMutationBody =
| BodyType<DashboardtypesPostableDashboardViewDTO>
| undefined;
export type CreateDashboardViewMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create dashboard saved view
*/
export const useCreateDashboardView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDashboardView>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createDashboardView>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
TContext
> => {
return useMutation(getCreateDashboardViewMutationOptions(options));
};
/**
* Removes a saved view. Saved views are shared org-wide. Deleting a non-existent view returns 404.
* @summary Delete dashboard saved view
*/
export const deleteDashboardView = (
{ id }: DeleteDashboardViewPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v2/dashboard_views/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteDashboardViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteDashboardView>>,
TError,
{ pathParams: DeleteDashboardViewPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteDashboardView>>,
TError,
{ pathParams: DeleteDashboardViewPathParameters },
TContext
> => {
const mutationKey = ['deleteDashboardView'];
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 deleteDashboardView>>,
{ pathParams: DeleteDashboardViewPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteDashboardView(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteDashboardViewMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteDashboardView>>
>;
export type DeleteDashboardViewMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete dashboard saved view
*/
export const useDeleteDashboardView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteDashboardView>>,
TError,
{ pathParams: DeleteDashboardViewPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteDashboardView>>,
TError,
{ pathParams: DeleteDashboardViewPathParameters },
TContext
> => {
return useMutation(getDeleteDashboardViewMutationOptions(options));
};
/**
* Replaces a saved view's name and data. Saved views are shared org-wide.
* @summary Update dashboard saved view
*/
export const updateDashboardView = (
{ id }: UpdateDashboardViewPathParameters,
dashboardtypesPostableDashboardViewDTO?: BodyType<DashboardtypesPostableDashboardViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<UpdateDashboardView200>({
url: `/api/v2/dashboard_views/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: dashboardtypesPostableDashboardViewDTO,
signal,
});
};
export const getUpdateDashboardViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateDashboardView>>,
TError,
{
pathParams: UpdateDashboardViewPathParameters;
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateDashboardView>>,
TError,
{
pathParams: UpdateDashboardViewPathParameters;
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
},
TContext
> => {
const mutationKey = ['updateDashboardView'];
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 updateDashboardView>>,
{
pathParams: UpdateDashboardViewPathParameters;
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateDashboardView(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateDashboardViewMutationResult = NonNullable<
Awaited<ReturnType<typeof updateDashboardView>>
>;
export type UpdateDashboardViewMutationBody =
| BodyType<DashboardtypesPostableDashboardViewDTO>
| undefined;
export type UpdateDashboardViewMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update dashboard saved view
*/
export const useUpdateDashboardView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateDashboardView>>,
TError,
{
pathParams: UpdateDashboardViewPathParameters;
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateDashboardView>>,
TError,
{
pathParams: UpdateDashboardViewPathParameters;
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
},
TContext
> => {
return useMutation(getUpdateDashboardViewMutationOptions(options));
};
/**
* Returns a page of v2-shape dashboards for the org. This is the pure, user-independent list — it carries no pin state. Use ListDashboardsForUserV2 for the personalized, pin-aware list. Supports a filter DSL (`query`), sort (`updated_at`/`created_at`/`name`), order (`asc`/`desc`), and offset-based pagination (`limit`/`offset`).
* @summary List dashboards (v2)

View File

@@ -4635,54 +4635,6 @@ export interface DashboardtypesDashboardSpecDTO {
variables: DashboardtypesVariableDTO[];
}
export enum DashboardtypesListOrderDTO {
asc = 'asc',
desc = 'desc',
}
export enum DashboardtypesListSortDTO {
updated_at = 'updated_at',
created_at = 'created_at',
name = 'name',
}
export interface DashboardtypesDashboardViewDataDTO {
order?: DashboardtypesListOrderDTO;
/**
* @type string
*/
query?: string;
sort?: DashboardtypesListSortDTO;
/**
* @type string
*/
version: string;
}
export interface DashboardtypesDashboardViewDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
data: DashboardtypesDashboardViewDataDTO;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
}
export enum DashboardtypesDatasourcePluginKindDTO {
'signoz/Datasource' = 'signoz/Datasource',
}
@@ -4794,6 +4746,15 @@ export interface DashboardtypesJSONPatchOperationDTO {
value?: unknown;
}
export enum DashboardtypesListOrderDTO {
asc = 'asc',
desc = 'desc',
}
export enum DashboardtypesListSortDTO {
updated_at = 'updated_at',
created_at = 'created_at',
name = 'name',
}
export interface DashboardtypesListedDashboardV2SpecDTO {
display?: DashboardtypesDisplayDTO;
}
@@ -4936,13 +4897,6 @@ export interface DashboardtypesListableDashboardV2DTO {
total: number;
}
export interface DashboardtypesListableDashboardViewDTO {
/**
* @type array
*/
views: DashboardtypesDashboardViewDTO[];
}
export enum DashboardtypesPanelPluginKindDTO {
'signoz/TimeSeriesPanel' = 'signoz/TimeSeriesPanel',
'signoz/BarChartPanel' = 'signoz/BarChartPanel',
@@ -4994,14 +4948,6 @@ export interface DashboardtypesPostableDashboardV2DTO {
tags: TagtypesPostableTagDTO[] | null;
}
export interface DashboardtypesPostableDashboardViewDTO {
data: DashboardtypesDashboardViewDataDTO;
/**
* @type string
*/
name: string;
}
export interface DashboardtypesPostablePublicDashboardDTO {
/**
* @type string
@@ -9880,36 +9826,6 @@ export type GetUserPreference200 = {
export type UpdateUserPreferencePathParameters = {
name: string;
};
export type ListDashboardViews200 = {
data: DashboardtypesListableDashboardViewDTO;
/**
* @type string
*/
status: string;
};
export type CreateDashboardView201 = {
data: DashboardtypesDashboardViewDTO;
/**
* @type string
*/
status: string;
};
export type DeleteDashboardViewPathParameters = {
id: string;
};
export type UpdateDashboardViewPathParameters = {
id: string;
};
export type UpdateDashboardView200 = {
data: DashboardtypesDashboardViewDTO;
/**
* @type string
*/
status: string;
};
export type ListDashboardsV2Params = {
/**
* @type string

View File

@@ -74,6 +74,17 @@ describe('RouteTab component', () => {
expect(history.location.pathname).toBe('/tab2');
});
it('does not animate tab panels to prevent CSSMotion DOM corruption', () => {
const history = createMemoryHistory();
const { container } = render(
<Router history={history}>
<RouteTab history={history} routes={testRoutes} activeKey="Tab1" />
</Router>,
);
const tabContent = container.querySelector('.ant-tabs-content');
expect(tabContent).not.toHaveClass('ant-tabs-content-animated');
});
it('calls onChangeHandler on tab change', () => {
const onChangeHandler = jest.fn();
const history = createMemoryHistory();

View File

@@ -59,7 +59,7 @@ function RouteTab({
destroyInactiveTabPane
activeKey={currentRoute?.key || activeKey}
defaultActiveKey={currentRoute?.key || activeKey}
animated
animated={{ inkBar: true, tabPane: false }}
items={items}
tabBarExtraContent={
showRightSection && (

View File

@@ -0,0 +1,41 @@
import { useQueries } from 'react-query';
import { render, screen } from 'tests/test-utils';
import GeneralSettings from '../index';
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useQueries: jest.fn(),
}));
const baseQueryResult = {
isError: false,
isLoading: false,
isFetching: false,
isSuccess: true,
data: undefined,
error: null,
refetch: jest.fn(),
};
describe('GeneralSettings index', () => {
it('renders fallback message when logs query fails with a non-APIError', () => {
(useQueries as jest.Mock).mockReturnValue([
{ ...baseQueryResult },
{ ...baseQueryResult },
{
...baseQueryResult,
isError: true,
isSuccess: false,
error: new TypeError(
"Cannot read properties of undefined (reading 'code')",
),
},
{ ...baseQueryResult },
]);
render(<GeneralSettings />);
expect(screen.getByText('something_went_wrong')).toBeInTheDocument();
});
});

View File

@@ -76,7 +76,9 @@ function GeneralSettings(): JSX.Element {
if (getRetentionPeriodLogsApiResponse.isError || getDisksResponse.isError) {
return (
<Typography>
{(getRetentionPeriodLogsApiResponse.error as APIError).getErrorMessage() ||
{(getRetentionPeriodLogsApiResponse.error instanceof APIError
? getRetentionPeriodLogsApiResponse.error.getErrorMessage()
: undefined) ||
getDisksResponse.data?.error ||
t('something_went_wrong')}
</Typography>

View File

@@ -212,74 +212,6 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/dashboard_views", handler.New(provider.authzMiddleware.ViewAccess(provider.dashboardHandler.ListViews), handler.OpenAPIDef{
ID: "ListDashboardViews",
Tags: []string{"dashboard"},
Summary: "List dashboard saved views",
Description: "Returns every saved view in the calling user's org. Saved views are shared org-wide.",
Request: nil,
RequestContentType: "",
Response: new(dashboardtypes.ListableDashboardView),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/dashboard_views", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.CreateView), handler.OpenAPIDef{
ID: "CreateDashboardView",
Tags: []string{"dashboard"},
Summary: "Create dashboard saved view",
Description: "Persists the calling user's dashboard listing state (query, sort, order) as a named, reusable view shared across the org.",
Request: new(dashboardtypes.PostableDashboardView),
RequestContentType: "application/json",
Response: new(dashboardtypes.DashboardView),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/dashboard_views/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.UpdateView), handler.OpenAPIDef{
ID: "UpdateDashboardView",
Tags: []string{"dashboard"},
Summary: "Update dashboard saved view",
Description: "Replaces a saved view's name and data. Saved views are shared org-wide.",
Request: new(dashboardtypes.UpdatableDashboardView),
RequestContentType: "application/json",
Response: new(dashboardtypes.DashboardView),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/dashboard_views/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.DeleteView), handler.OpenAPIDef{
ID: "DeleteDashboardView",
Tags: []string{"dashboard"},
Summary: "Delete dashboard saved view",
Description: "Removes a saved view. Saved views are shared org-wide. Deleting a non-existent view returns 404.",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.CreatePublic), handler.OpenAPIDef{
ID: "CreatePublicDashboard",
Tags: []string{"dashboard"},

View File

@@ -78,14 +78,6 @@ type Module interface {
DeleteV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
DeletePreferencesForUser(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error
CreateView(ctx context.Context, orgID valuer.UUID, postable dashboardtypes.PostableDashboardView) (*dashboardtypes.DashboardView, error)
ListViews(ctx context.Context, orgID valuer.UUID) (*dashboardtypes.ListableDashboardView, error)
UpdateView(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updateable dashboardtypes.UpdatableDashboardView) (*dashboardtypes.DashboardView, error)
DeleteView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
}
type Handler interface {
@@ -133,12 +125,4 @@ type Handler interface {
UnpinV2(http.ResponseWriter, *http.Request)
DeleteV2(http.ResponseWriter, *http.Request)
CreateView(http.ResponseWriter, *http.Request)
ListViews(http.ResponseWriter, *http.Request)
UpdateView(http.ResponseWriter, *http.Request)
DeleteView(http.ResponseWriter, *http.Request)
}

View File

@@ -472,92 +472,3 @@ func (store *store) DeletePreferencesForUser(ctx context.Context, orgID valuer.U
}
return nil
}
func (store *store) CreateDashboardView(ctx context.Context, view *dashboardtypes.DashboardView) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(view).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "dashboard view with id %s already exists", view.ID)
}
return nil
}
func (store *store) GetDashboardView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardView, error) {
view := new(dashboardtypes.DashboardView)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(view).
Where("id = ?", id).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, dashboardtypes.ErrCodeDashboardViewNotFound, "dashboard view with id %s doesn't exist", id)
}
return view, nil
}
func (store *store) ListDashboardViews(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.DashboardView, error) {
views := make([]*dashboardtypes.DashboardView, 0)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(&views).
Where("org_id = ?", orgID).
OrderExpr("updated_at DESC").
Scan(ctx)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't list dashboard views")
}
return views, nil
}
func (store *store) UpdateDashboardView(ctx context.Context, view *dashboardtypes.DashboardView) error {
res, err := store.
sqlstore.
BunDBCtx(ctx).
NewUpdate().
Model(view).
WherePK().
Where("org_id = ?", view.OrgID).
Exec(ctx)
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't update dashboard view")
}
rows, err := res.RowsAffected()
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't read dashboard view update result")
}
if rows == 0 {
return errors.Newf(errors.TypeNotFound, dashboardtypes.ErrCodeDashboardViewNotFound, "dashboard view with id %s doesn't exist", view.ID)
}
return nil
}
func (store *store) DeleteDashboardView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
res, err := store.
sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(dashboardtypes.DashboardView)).
Where("id = ?", id).
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't delete dashboard view")
}
rows, err := res.RowsAffected()
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't read dashboard view delete result")
}
if rows == 0 {
return errors.Newf(errors.TypeNotFound, dashboardtypes.ErrCodeDashboardViewNotFound, "dashboard view with id %s doesn't exist", id)
}
return nil
}

View File

@@ -1,132 +0,0 @@
package impldashboard
import (
"context"
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
func (handler *handler) CreateView(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 := valuer.MustNewUUID(claims.OrgID)
var req dashboardtypes.PostableDashboardView
if err := binding.JSON.BindBody(r.Body, &req); err != nil {
render.Error(rw, err)
return
}
view, err := handler.module.CreateView(ctx, orgID, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, view)
}
func (handler *handler) ListViews(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 := valuer.MustNewUUID(claims.OrgID)
out, err := handler.module.ListViews(ctx, orgID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, out)
}
func (handler *handler) UpdateView(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 := valuer.MustNewUUID(claims.OrgID)
id := mux.Vars(r)["id"]
if id == "" {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
return
}
viewID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
var req dashboardtypes.UpdatableDashboardView
if err := binding.JSON.BindBody(r.Body, &req); err != nil {
render.Error(rw, err)
return
}
view, err := handler.module.UpdateView(ctx, orgID, viewID, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, view)
}
func (handler *handler) DeleteView(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 := valuer.MustNewUUID(claims.OrgID)
id := mux.Vars(r)["id"]
if id == "" {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
return
}
viewID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
if err := handler.module.DeleteView(ctx, orgID, viewID); err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}

View File

@@ -1,46 +0,0 @@
package impldashboard
import (
"context"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
func (module *module) CreateView(ctx context.Context, orgID valuer.UUID, postable dashboardtypes.PostableDashboardView) (*dashboardtypes.DashboardView, error) {
if err := postable.Validate(); err != nil {
return nil, err
}
view := postable.NewDashboardView(orgID)
if err := module.store.CreateDashboardView(ctx, view); err != nil {
return nil, err
}
return view, nil
}
func (module *module) ListViews(ctx context.Context, orgID valuer.UUID) (*dashboardtypes.ListableDashboardView, error) {
views, err := module.store.ListDashboardViews(ctx, orgID)
if err != nil {
return nil, err
}
return &dashboardtypes.ListableDashboardView{Views: views}, nil
}
func (module *module) UpdateView(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updateable dashboardtypes.UpdatableDashboardView) (*dashboardtypes.DashboardView, error) {
if err := updateable.Validate(); err != nil {
return nil, err
}
view, err := module.store.GetDashboardView(ctx, orgID, id)
if err != nil {
return nil, err
}
view.Update(updateable)
if err := module.store.UpdateDashboardView(ctx, view); err != nil {
return nil, err
}
return view, nil
}
func (module *module) DeleteView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
return module.store.DeleteDashboardView(ctx, orgID, id)
}

View File

@@ -213,7 +213,6 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewCloudIntegrationRemoveCascadeDeleteFactory(sqlschema),
sqlmigration.NewAddUserDashboardPreferenceFactory(sqlstore, sqlschema),
sqlmigration.NewRecreateUserDashboardPreferenceFactory(sqlstore, sqlschema),
sqlmigration.NewAddDashboardViewFactory(sqlstore, sqlschema),
)
}

View File

@@ -1,69 +0,0 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type addDashboardView struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewAddDashboardViewFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_dashboard_view"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addDashboardView{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
})
}
func (migration *addDashboardView) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addDashboardView) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
sqls := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "dashboard_view",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "name", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "data", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}},
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
{
ReferencingColumnName: sqlschema.ColumnName("org_id"),
ReferencedTableName: sqlschema.TableName("organizations"),
ReferencedColumnName: sqlschema.ColumnName("id"),
},
},
})
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
return tx.Commit()
}
func (migration *addDashboardView) Down(_ context.Context, _ *bun.DB) error {
return nil
}

View File

@@ -1,125 +0,0 @@
package dashboardtypes
import (
"bytes"
"encoding/json"
"strings"
"time"
"unicode/utf8"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
const (
DashboardViewSchemaVersion = "v1"
MaxDashboardViewNameLen = 32
)
var (
ErrCodeDashboardViewInvalidInput = errors.MustNewCode("dashboard_view_invalid_input")
ErrCodeDashboardViewNotFound = errors.MustNewCode("dashboard_view_not_found")
)
type DashboardView struct {
bun.BaseModel `bun:"table:dashboard_view,alias:dashboard_view"`
types.Identifiable
types.TimeAuditable
Name string `bun:"name,type:text,notnull" json:"name" required:"true"`
Data DashboardViewData `bun:"data,type:text,notnull" json:"data" required:"true"`
OrgID valuer.UUID `bun:"org_id,type:text,notnull" json:"orgId" required:"true"`
}
type DashboardViewData struct {
Version string `json:"version" required:"true"`
ListFilter
}
func (d *DashboardViewData) Validate() error {
if d.Version != DashboardViewSchemaVersion {
return errors.NewInvalidInputf(ErrCodeDashboardViewInvalidInput,
"version must be %q, got %q", DashboardViewSchemaVersion, d.Version)
}
return d.ListFilter.Validate()
}
// ════════════════════════════════════════════════════════════════════════
// Postable
// ════════════════════════════════════════════════════════════════════════
type PostableDashboardView struct {
Name string `json:"name" required:"true"`
Data DashboardViewData `json:"data" required:"true"`
}
func (p *PostableDashboardView) UnmarshalJSON(data []byte) error {
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
type alias PostableDashboardView
var tmp alias
if err := dec.Decode(&tmp); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardViewInvalidInput, "invalid saved view request body").WithAdditional(err.Error())
}
*p = PostableDashboardView(tmp)
return p.Validate()
}
func (p *PostableDashboardView) Validate() error {
name, err := trimAndValidateDashboardViewName(p.Name)
if err != nil {
return err
}
p.Name = name
return p.Data.Validate()
}
func (p PostableDashboardView) NewDashboardView(orgID valuer.UUID) *DashboardView {
now := time.Now()
return &DashboardView{
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
TimeAuditable: types.TimeAuditable{CreatedAt: now, UpdatedAt: now},
OrgID: orgID,
Name: p.Name,
Data: p.Data,
}
}
// ════════════════════════════════════════════════════════════════════════
// Updateable
// ════════════════════════════════════════════════════════════════════════
type UpdatableDashboardView = PostableDashboardView
func (v *DashboardView) Update(updateable UpdatableDashboardView) {
v.Name = updateable.Name
v.Data = updateable.Data
v.UpdatedAt = time.Now()
}
// ════════════════════════════════════════════════════════════════════════
// Listable
// ════════════════════════════════════════════════════════════════════════
type ListableDashboardView struct {
Views []*DashboardView `json:"views" required:"true" nullable:"false"`
}
// ════════════════════════════════════════════════════════════════════════
// Helpers
// ════════════════════════════════════════════════════════════════════════
func trimAndValidateDashboardViewName(name string) (string, error) {
trimmed := strings.TrimSpace(name)
if trimmed == "" {
return "", errors.NewInvalidInputf(ErrCodeDashboardViewInvalidInput, "name is required")
}
if n := utf8.RuneCountInString(trimmed); n > MaxDashboardViewNameLen {
return "", errors.NewInvalidInputf(ErrCodeDashboardViewInvalidInput,
"name must be at most %d characters, got %d", MaxDashboardViewNameLen, n)
}
return trimmed, nil
}

View File

@@ -1,152 +0,0 @@
package dashboardtypes
import (
"encoding/json"
"strings"
"testing"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDashboardViewDataValidate(t *testing.T) {
cases := []struct {
description string
data DashboardViewData
expectError bool
}{
{
description: "valid with all fields set",
data: DashboardViewData{Version: DashboardViewSchemaVersion, ListFilter: ListFilter{Query: "name=foo", Sort: ListSortName, Order: ListOrderAsc}},
expectError: false,
},
{
description: "query over the cap is rejected",
data: DashboardViewData{Version: DashboardViewSchemaVersion, ListFilter: ListFilter{Query: strings.Repeat("x", MaxListQueryLen+1)}},
expectError: true,
},
{
description: "valid with zero sort and order",
data: DashboardViewData{Version: DashboardViewSchemaVersion},
expectError: false,
},
{
description: "wrong version is rejected",
data: DashboardViewData{Version: "v2"},
expectError: true,
},
{
description: "empty version is rejected",
data: DashboardViewData{},
expectError: true,
},
{
description: "unknown sort is rejected",
data: DashboardViewData{Version: DashboardViewSchemaVersion, ListFilter: ListFilter{Sort: ListSort{valuer.NewString("bogus")}}},
expectError: true,
},
{
description: "unknown order is rejected",
data: DashboardViewData{Version: DashboardViewSchemaVersion, ListFilter: ListFilter{Order: ListOrder{valuer.NewString("sideways")}}},
expectError: true,
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
err := c.data.Validate()
if c.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestPostableDashboardViewUnmarshalJSON(t *testing.T) {
cases := []struct {
description string
body string
expectError bool
expectedName string
}{
{
description: "valid body trims surrounding whitespace in name",
body: `{"name":" my view ","data":{"version":"v1","sort":"name","order":"asc"}}`,
expectError: false,
expectedName: "my view",
},
{
description: "unknown field is rejected",
body: `{"name":"my view","data":{"version":"v1"},"extra":true}`,
expectError: true,
},
{
description: "blank name is rejected",
body: `{"name":" ","data":{"version":"v1"}}`,
expectError: true,
},
{
description: "name over max length is rejected",
body: `{"name":"` + strings.Repeat("x", MaxDashboardViewNameLen+1) + `","data":{"version":"v1"}}`,
expectError: true,
},
{
description: "invalid data version is rejected",
body: `{"name":"my view","data":{"version":"v9"}}`,
expectError: true,
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
var p PostableDashboardView
err := json.Unmarshal([]byte(c.body), &p)
if c.expectError {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, c.expectedName, p.Name)
})
}
}
func TestPostableDashboardViewNewDashboardView(t *testing.T) {
orgID := valuer.GenerateUUID()
postable := PostableDashboardView{
Name: "my view",
Data: DashboardViewData{Version: DashboardViewSchemaVersion, ListFilter: ListFilter{Sort: ListSortName, Order: ListOrderAsc}},
}
view := postable.NewDashboardView(orgID)
assert.Equal(t, orgID, view.OrgID)
assert.Equal(t, "my view", view.Name)
assert.Equal(t, postable.Data, view.Data)
assert.False(t, view.ID.IsZero())
assert.False(t, view.CreatedAt.IsZero())
assert.Equal(t, view.CreatedAt, view.UpdatedAt)
}
func TestDashboardViewUpdate(t *testing.T) {
orgID := valuer.GenerateUUID()
view := PostableDashboardView{
Name: "original",
Data: DashboardViewData{Version: DashboardViewSchemaVersion, ListFilter: ListFilter{Sort: ListSortName, Order: ListOrderAsc}},
}.NewDashboardView(orgID)
createdAt := view.CreatedAt
view.Update(UpdatableDashboardView{
Name: "renamed",
Data: DashboardViewData{Version: DashboardViewSchemaVersion, ListFilter: ListFilter{Sort: ListSortCreatedAt, Order: ListOrderDesc}},
})
assert.Equal(t, "renamed", view.Name)
assert.Equal(t, ListSortCreatedAt, view.Data.Sort)
assert.Equal(t, ListOrderDesc, view.Data.Order)
assert.Equal(t, createdAt, view.CreatedAt)
assert.True(t, view.UpdatedAt.After(createdAt) || view.UpdatedAt.Equal(createdAt))
}

View File

@@ -2,7 +2,6 @@ package dashboardtypes
import (
"slices"
"unicode/utf8"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
@@ -13,7 +12,6 @@ import (
const (
DefaultListLimit = 20
MaxListLimit = 200
MaxListQueryLen = 1024
)
// ListSort is the sort field for the dashboard list endpoint. The value is a
@@ -51,45 +49,31 @@ func (o ListOrder) IsValid() bool {
var ErrCodeDashboardListInvalid = errors.MustNewCode("dashboard_list_invalid")
type ListFilter struct {
Query string `query:"query" json:"query"`
Sort ListSort `query:"sort" json:"sort"`
Order ListOrder `query:"order" json:"order"`
}
func (f *ListFilter) Validate() error {
if n := utf8.RuneCountInString(f.Query); n > MaxListQueryLen {
return errors.NewInvalidInputf(ErrCodeDashboardListInvalid,
"query cannot be longer than %d characters, got %d", MaxListQueryLen, n)
}
if !f.Sort.IsZero() && !f.Sort.IsValid() {
return errors.NewInvalidInputf(ErrCodeDashboardListInvalid,
"invalid sort %q — expected one of: `updated_at`, `created_at`, `name`", f.Sort)
}
if !f.Order.IsZero() && !f.Order.IsValid() {
return errors.NewInvalidInputf(ErrCodeDashboardListInvalid,
"invalid order %q — expected `asc` or `desc`", f.Order)
}
return nil
}
type ListDashboardsV2Params struct {
ListFilter
Limit int `query:"limit"`
Offset int `query:"offset"`
Query string `query:"query"`
Sort ListSort `query:"sort"`
Order ListOrder `query:"order"`
Limit int `query:"limit"`
Offset int `query:"offset"`
}
// Validate fills in defaults (sort=updated_at, order=desc, limit=20) and
// rejects out-of-allowlist sort/order values and bad limit/offset. Limit is
// clamped to MaxListLimit on the high side. Sort/order are case-insensitive —
// valuer.String lowercases them at bind time.
func (p *ListDashboardsV2Params) Validate() error {
if err := p.ListFilter.Validate(); err != nil {
return err
}
if p.Sort.IsZero() {
p.Sort = ListSortUpdatedAt
} else if !p.Sort.IsValid() {
return errors.NewInvalidInputf(ErrCodeDashboardListInvalid,
"invalid sort %q — expected one of: `updated_at`, `created_at`, `name`", p.Sort)
}
if p.Order.IsZero() {
p.Order = ListOrderDesc
} else if !p.Order.IsValid() {
return errors.NewInvalidInputf(ErrCodeDashboardListInvalid,
"invalid order %q — expected `asc` or `desc`", p.Order)
}
if p.Limit == 0 {

View File

@@ -1,35 +0,0 @@
package dashboardtypes
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestListDashboardsV2ParamsValidate(t *testing.T) {
t.Run("fills defaults when sort, order and limit are unset", func(t *testing.T) {
p := &ListDashboardsV2Params{}
require.NoError(t, p.Validate())
assert.Equal(t, ListSortUpdatedAt, p.Sort)
assert.Equal(t, ListOrderDesc, p.Order)
assert.Equal(t, DefaultListLimit, p.Limit)
})
t.Run("clamps limit to MaxListLimit", func(t *testing.T) {
p := &ListDashboardsV2Params{Limit: MaxListLimit + 50}
require.NoError(t, p.Validate())
assert.Equal(t, MaxListLimit, p.Limit)
})
t.Run("query at the cap is accepted", func(t *testing.T) {
p := &ListDashboardsV2Params{ListFilter: ListFilter{Query: strings.Repeat("x", MaxListQueryLen)}}
assert.NoError(t, p.Validate())
})
t.Run("query over the cap is rejected", func(t *testing.T) {
p := &ListDashboardsV2Params{ListFilter: ListFilter{Query: strings.Repeat("x", MaxListQueryLen+1)}}
assert.Error(t, p.Validate())
})
}

View File

@@ -51,17 +51,4 @@ type Store interface {
DeletePreferencesForDashboard(ctx context.Context, orgID valuer.UUID, dashboardID valuer.UUID) error
DeletePreferencesForUser(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error
// ════════════════════════════════════════════════════════════════════════
// Dashboard saved view methods
// ════════════════════════════════════════════════════════════════════════
CreateDashboardView(ctx context.Context, view *DashboardView) error
GetDashboardView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*DashboardView, error)
ListDashboardViews(ctx context.Context, orgID valuer.UUID) ([]*DashboardView, error)
UpdateDashboardView(ctx context.Context, view *DashboardView) error
DeleteDashboardView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
}