mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-02 12:50:37 +01:00
Compare commits
1 Commits
fix/panel-
...
refactor/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00f23273cf |
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
@@ -109,10 +109,7 @@ go.mod @therealpandey
|
||||
/pkg/modules/role/ @therealpandey
|
||||
/pkg/types/coretypes/ @therealpandey @vikrantgupta25
|
||||
|
||||
/frontend/src/hooks/useAuthZ/ @H4ad
|
||||
/frontend/src/components/GuardAuthZ/ @H4ad
|
||||
/frontend/src/components/AuthZTooltip/ @H4ad
|
||||
/frontend/src/components/createGuardedRoute/ @H4ad
|
||||
/frontend/src/lib/authz/ @H4ad
|
||||
/frontend/src/container/RolesSettings/ @H4ad
|
||||
/frontend/src/components/RolesSelect/ @H4ad
|
||||
/frontend/src/pages/MembersSettings/ @H4ad
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const permissionsTypePath = "frontend/src/hooks/useAuthZ/permissions.type.ts"
|
||||
const permissionsTypePath = "frontend/src/lib/authz/hooks/useAuthZ/permissions.type.ts"
|
||||
|
||||
var permissionsTypeTemplate = template.Must(template.New("permissions").Parse(
|
||||
`// AUTO GENERATED FILE - DO NOT EDIT - GENERATED BY cmd/enterprise/*.go generate authz
|
||||
|
||||
@@ -328,11 +328,6 @@
|
||||
{
|
||||
"name": "immer",
|
||||
"message": "[State mgmt] Direct immer usage is deprecated. Use Zustand (which integrates immer via the immer middleware) instead."
|
||||
},
|
||||
{
|
||||
"name": "api/generated/services/dashboard",
|
||||
"importNames": ["patchDashboardV2", "usePatchDashboardV2"],
|
||||
"message": "[dashboard-v2] Don't call patchDashboardV2/usePatchDashboardV2 directly — use useOptimisticPatch().patchAsync so spec edits update the react-query cache optimistically and reconcile on settle."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Controller, useForm } from 'react-hook-form';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
import { SACreatePermission } from 'hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import { SACreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { DialogFooter, DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
|
||||
import CreateServiceAccountModal from '../CreateServiceAccountModal';
|
||||
|
||||
jest.mock('components/AuthZTooltip/AuthZTooltip', () => ({
|
||||
jest.mock('lib/authz/components/AuthZTooltip/AuthZTooltip', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
children,
|
||||
|
||||
@@ -4,11 +4,11 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { DatePicker } from 'antd';
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import {
|
||||
APIKeyCreatePermission,
|
||||
buildSAAttachPermission,
|
||||
} from 'hooks/useAuthZ/permissions/service-account.permissions';
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { disabledDate } from '../utils';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Trash2, X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
import { buildSADeletePermission } from 'hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import { buildSADeletePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
|
||||
@@ -7,12 +7,12 @@ import { Input } from '@signozhq/ui/input';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { DatePicker } from 'antd';
|
||||
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import {
|
||||
buildAPIKeyDeletePermission,
|
||||
buildAPIKeyUpdatePermission,
|
||||
buildSADetachPermission,
|
||||
} from 'hooks/useAuthZ/permissions/service-account.permissions';
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { disabledDate, formatLastObservedAt } from '../utils';
|
||||
|
||||
@@ -16,8 +16,8 @@ import type {
|
||||
import { AxiosError } from 'axios';
|
||||
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
|
||||
import dayjs from 'dayjs';
|
||||
import { buildAPIKeyUpdatePermission } from 'hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { buildAPIKeyUpdatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
@@ -4,13 +4,13 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { Skeleton, Table, Tooltip } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table/interface';
|
||||
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import {
|
||||
APIKeyCreatePermission,
|
||||
buildAPIKeyDeletePermission,
|
||||
buildSAAttachPermission,
|
||||
buildSADetachPermission,
|
||||
} from 'hooks/useAuthZ/permissions/service-account.permissions';
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import dayjs from 'dayjs';
|
||||
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs';
|
||||
|
||||
@@ -5,11 +5,11 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import RolesSelect from 'components/RolesSelect';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils';
|
||||
import { buildSAUpdatePermission } from 'hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { buildSAUpdatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Trash2, X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import {
|
||||
buildAPIKeyDeletePermission,
|
||||
buildSADetachPermission,
|
||||
} from 'hooks/useAuthZ/permissions/service-account.permissions';
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import PermissionDeniedCallout from 'components/PermissionDeniedCallout/PermissionDeniedCallout';
|
||||
import PermissionDeniedCallout from 'lib/authz/components/PermissionDeniedCallout/PermissionDeniedCallout';
|
||||
import { useRoles } from 'components/RolesSelect';
|
||||
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
|
||||
import {
|
||||
@@ -35,8 +35,8 @@ import {
|
||||
buildSADeletePermission,
|
||||
buildSAReadPermission,
|
||||
buildSAUpdatePermission,
|
||||
} from 'hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import {
|
||||
parseAsBoolean,
|
||||
parseAsInteger,
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
import APIError from 'types/api/error';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import AddKeyModal from './AddKeyModal';
|
||||
import DeleteAccountModal from './DeleteAccountModal';
|
||||
import KeysTab from './KeysTab';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import EditKeyModal from '../EditKeyModal';
|
||||
|
||||
jest.mock('components/AuthZTooltip/AuthZTooltip', () => ({
|
||||
jest.mock('lib/authz/components/AuthZTooltip/AuthZTooltip', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
children,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import KeysTab from '../KeysTab';
|
||||
|
||||
jest.mock('components/AuthZTooltip/AuthZTooltip', () => ({
|
||||
jest.mock('lib/authz/components/AuthZTooltip/AuthZTooltip', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
children,
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
setupAuthzAdmin,
|
||||
setupAuthzDeny,
|
||||
setupAuthzDenyAll,
|
||||
} from 'tests/authz-test-utils';
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
import {
|
||||
APIKeyListPermission,
|
||||
buildSADeletePermission,
|
||||
} from 'hooks/useAuthZ/permissions/service-account.permissions';
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
|
||||
import ServiceAccountDrawer from '../ServiceAccountDrawer';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { setupAuthzAdmin } from 'tests/authz-test-utils';
|
||||
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import ServiceAccountDrawer from '../ServiceAccountDrawer';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||
import { SolidAlertTriangle } from '@signozhq/icons';
|
||||
import { Select, Tooltip } from 'antd';
|
||||
import type { DefaultOptionType } from 'antd/es/select';
|
||||
import cx from 'classnames';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { UniversalYAxisUnitMappings } from './constants';
|
||||
import { UniversalYAxisUnit, YAxisUnitSelectorProps } from './types';
|
||||
@@ -72,7 +72,9 @@ function YAxisUnitSelector({
|
||||
}, [categoriesOverride, source]);
|
||||
|
||||
return (
|
||||
<div className={cx('y-axis-unit-selector-component', containerClassName)}>
|
||||
<div
|
||||
className={classNames('y-axis-unit-selector-component', containerClassName)}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
value={universalUnit}
|
||||
@@ -82,17 +84,12 @@ function YAxisUnitSelector({
|
||||
loading={loading}
|
||||
suffixIcon={
|
||||
incompatibleUnitMessage ? (
|
||||
<Tooltip
|
||||
title={incompatibleUnitMessage}
|
||||
overlayClassName="y-axis-unit-warning-tooltip"
|
||||
>
|
||||
<span className="y-axis-unit-warning" role="img" aria-label="warning">
|
||||
<SolidAlertTriangle size="md" />
|
||||
</span>
|
||||
<Tooltip title={incompatibleUnitMessage}>
|
||||
<SolidAlertTriangle role="img" aria-label="warning" size="md" />
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
className={cx({
|
||||
className={classNames({
|
||||
'warning-state': incompatibleUnitMessage,
|
||||
})}
|
||||
data-testid={dataTestId}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { YAxisCategoryNames } from '../constants';
|
||||
import { UniversalYAxisUnit, YAxisSource } from '../types';
|
||||
@@ -7,13 +6,9 @@ import YAxisUnitSelector from '../YAxisUnitSelector';
|
||||
|
||||
describe('YAxisUnitSelector', () => {
|
||||
const mockOnChange = jest.fn();
|
||||
// antd injects its `pointer-events` styles via cssinjs in jsdom, but the SCSS
|
||||
// overrides aren't loaded — skip the pointer-events check so hovers/clicks register.
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockOnChange.mockClear();
|
||||
user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
});
|
||||
|
||||
it('renders with default placeholder', () => {
|
||||
@@ -39,7 +34,7 @@ describe('YAxisUnitSelector', () => {
|
||||
expect(screen.queryByText('Custom placeholder')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onChange when a value is selected', async () => {
|
||||
it('calls onChange when a value is selected', () => {
|
||||
render(
|
||||
<YAxisUnitSelector
|
||||
value=""
|
||||
@@ -49,8 +44,9 @@ describe('YAxisUnitSelector', () => {
|
||||
);
|
||||
const select = screen.getByRole('combobox');
|
||||
|
||||
await user.click(select);
|
||||
await user.click(screen.getByText('Bytes (B)'));
|
||||
fireEvent.mouseDown(select);
|
||||
const option = screen.getByText('Bytes (B)');
|
||||
fireEvent.click(option);
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('By', {
|
||||
children: 'Bytes (B)',
|
||||
@@ -59,7 +55,7 @@ describe('YAxisUnitSelector', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('filters options based on search input', async () => {
|
||||
it('filters options based on search input', () => {
|
||||
render(
|
||||
<YAxisUnitSelector
|
||||
value=""
|
||||
@@ -69,13 +65,14 @@ describe('YAxisUnitSelector', () => {
|
||||
);
|
||||
const select = screen.getByRole('combobox');
|
||||
|
||||
await user.click(select);
|
||||
await user.type(select, 'bytes/sec');
|
||||
fireEvent.mouseDown(select);
|
||||
const input = screen.getByRole('combobox');
|
||||
fireEvent.change(input, { target: { value: 'bytes/sec' } });
|
||||
|
||||
expect(screen.getByText('Bytes/sec')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows all categories and their units', async () => {
|
||||
it('shows all categories and their units', () => {
|
||||
render(
|
||||
<YAxisUnitSelector
|
||||
value=""
|
||||
@@ -83,8 +80,9 @@ describe('YAxisUnitSelector', () => {
|
||||
source={YAxisSource.ALERTS}
|
||||
/>,
|
||||
);
|
||||
const select = screen.getByRole('combobox');
|
||||
|
||||
await user.click(screen.getByRole('combobox'));
|
||||
fireEvent.mouseDown(select);
|
||||
|
||||
// Check for category headers
|
||||
expect(screen.getByText('Data')).toBeInTheDocument();
|
||||
@@ -95,7 +93,7 @@ describe('YAxisUnitSelector', () => {
|
||||
expect(screen.getByText('Seconds (s)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows warning message when incompatible unit is selected', async () => {
|
||||
it('shows warning message when incompatible unit is selected', () => {
|
||||
render(
|
||||
<YAxisUnitSelector
|
||||
source={YAxisSource.ALERTS}
|
||||
@@ -106,12 +104,12 @@ describe('YAxisUnitSelector', () => {
|
||||
);
|
||||
const warningIcon = screen.getByLabelText('warning');
|
||||
expect(warningIcon).toBeInTheDocument();
|
||||
await user.hover(warningIcon);
|
||||
await expect(
|
||||
screen.findByText(
|
||||
fireEvent.mouseOver(warningIcon);
|
||||
return screen
|
||||
.findByText(
|
||||
'Unit mismatch. The metric was sent with unit Seconds (s), but Bytes (B) is selected.',
|
||||
),
|
||||
).resolves.toBeInTheDocument();
|
||||
)
|
||||
.then((el) => expect(el).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('does not show warning message when compatible unit is selected', () => {
|
||||
@@ -127,7 +125,7 @@ describe('YAxisUnitSelector', () => {
|
||||
expect(warningIcon).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses categories override to render custom units', async () => {
|
||||
it('uses categories override to render custom units', () => {
|
||||
const customCategories = [
|
||||
{
|
||||
name: YAxisCategoryNames.Data,
|
||||
@@ -149,7 +147,9 @@ describe('YAxisUnitSelector', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('combobox'));
|
||||
const select = screen.getByRole('combobox');
|
||||
|
||||
fireEvent.mouseDown(select);
|
||||
|
||||
expect(screen.getByText('Custom Bytes (B)')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Bytes (B)')).not.toBeInTheDocument();
|
||||
|
||||
@@ -4,13 +4,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable hover on the warning icon: its `.ant-select-arrow` parent sets
|
||||
// `pointer-events: none`, which would otherwise suppress the tooltip.
|
||||
.y-axis-unit-warning {
|
||||
display: inline-flex;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.warning-state {
|
||||
.ant-select-selector {
|
||||
border-color: var(--bg-amber-400) !important;
|
||||
@@ -24,7 +17,3 @@
|
||||
right: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.y-axis-unit-warning-tooltip {
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
@@ -79,11 +79,13 @@ function Panel({
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
// Public data is fetched by index and the payload redacts each widget's
|
||||
// filters, so query bodies are identical across panels. Key on panel
|
||||
// identity + time — the only inputs that determine the response — so
|
||||
// panels don't collapse onto one cache entry.
|
||||
queryKey: [widget?.id, index, startTime, endTime],
|
||||
queryKey: [
|
||||
widget?.query,
|
||||
widget?.panelTypes,
|
||||
requestData,
|
||||
startTime,
|
||||
endTime,
|
||||
],
|
||||
retry(failureCount, error): boolean {
|
||||
if (
|
||||
String(error).includes('status: error') &&
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
import Panel from '../Panel';
|
||||
|
||||
const useGetQueryRangeMock = jest.fn();
|
||||
|
||||
jest.mock('hooks/queryBuilder/useGetQueryRange', () => ({
|
||||
useGetQueryRange: (...args: unknown[]): unknown => {
|
||||
useGetQueryRangeMock(...args);
|
||||
return {
|
||||
data: undefined,
|
||||
isFetching: false,
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('container/GridCardLayout/GridCard/WidgetGraphComponent', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="widget-graph" />,
|
||||
}));
|
||||
|
||||
const buildWidget = (id: string): Widgets =>
|
||||
({
|
||||
id,
|
||||
panelTypes: PANEL_TYPES.LIST,
|
||||
query: {
|
||||
builder: {
|
||||
queryData: [{ dataSource: 'logs', limit: 100, orderBy: [] }],
|
||||
},
|
||||
},
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
}) as unknown as Widgets;
|
||||
|
||||
describe('Public dashboard Panel', () => {
|
||||
beforeEach(() => {
|
||||
useGetQueryRangeMock.mockClear();
|
||||
});
|
||||
|
||||
it('keys each panel by widget id + index so identical queries do not collide (bug 5503)', () => {
|
||||
render(
|
||||
<>
|
||||
<Panel
|
||||
widget={buildWidget('widget-a')}
|
||||
index={2}
|
||||
dashboardId="dash-1"
|
||||
startTime={100}
|
||||
endTime={200}
|
||||
/>
|
||||
<Panel
|
||||
widget={buildWidget('widget-b')}
|
||||
index={62}
|
||||
dashboardId="dash-1"
|
||||
startTime={100}
|
||||
endTime={200}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
|
||||
const [callA, callB] = useGetQueryRangeMock.mock.calls;
|
||||
const queryKeyA = callA[2].queryKey;
|
||||
const metaA = callA[4];
|
||||
const queryKeyB = callB[2].queryKey;
|
||||
const metaB = callB[4];
|
||||
|
||||
// Key is panel identity + time only — the redacted query body is not part
|
||||
// of it, so identical query bodies can't collapse two panels onto one key.
|
||||
expect(queryKeyA).toStrictEqual(['widget-a', 2, 100, 200]);
|
||||
expect(queryKeyB).toStrictEqual(['widget-b', 62, 100, 200]);
|
||||
expect(queryKeyA).not.toStrictEqual(queryKeyB);
|
||||
|
||||
expect(metaA.widgetIndex).toBe(2);
|
||||
expect(metaB.widgetIndex).toBe(62);
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import { Input } from '@signozhq/ui/input';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Skeleton } from 'antd';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import PermissionDeniedFullPage from 'components/PermissionDeniedFullPage/PermissionDeniedFullPage';
|
||||
import PermissionDeniedFullPage from 'lib/authz/components/PermissionDeniedFullPage/PermissionDeniedFullPage';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useRolesFeatureGate } from 'hooks/useRolesFeatureGate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { defaultFeatureFlags, render, screen } from 'tests/test-utils';
|
||||
import { invalidLicense, mockUseAuthZGrantAll } from 'tests/authz-test-utils';
|
||||
import {
|
||||
invalidLicense,
|
||||
mockUseAuthZGrantAll,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import CreateEditRolePage from '../CreateEditRolePage';
|
||||
|
||||
jest.mock('hooks/useAuthZ/useAuthZ');
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import { mockUseAuthZDenyAll } from 'tests/authz-test-utils';
|
||||
import { mockUseAuthZDenyAll } from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import CreateEditRolePage from '../CreateEditRolePage';
|
||||
|
||||
jest.mock('hooks/useAuthZ/useAuthZ');
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -3,12 +3,12 @@ import ROUTES from 'constants/routes';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import CreateEditRolePage from '../CreateEditRolePage';
|
||||
|
||||
jest.mock('hooks/useAuthZ/useAuthZ');
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
const rolesApiBase = '*/api/v1/roles';
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import {
|
||||
mockUseAuthZDenyAll,
|
||||
mockUseAuthZGrantByPrefix,
|
||||
} from 'tests/authz-test-utils';
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import CreateEditRolePage from '../CreateEditRolePage';
|
||||
|
||||
jest.mock('hooks/useAuthZ/useAuthZ');
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
const EDIT_ROLE_ID = 'test-role-123';
|
||||
|
||||
@@ -4,12 +4,12 @@ import { customRoleResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import CreateEditRolePage from '../CreateEditRolePage';
|
||||
|
||||
jest.mock('hooks/useAuthZ/useAuthZ');
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
const CUSTOM_ROLE_ID = '019c24aa-3333-0001-aaaa-111111111111';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { render, screen, userEvent, within } from 'tests/test-utils';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import CreateEditRolePage from '../CreateEditRolePage';
|
||||
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
|
||||
jest.mock('hooks/useAuthZ/useAuthZ');
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
|
||||
import CreateEditRolePage from '../CreateEditRolePage';
|
||||
|
||||
jest.mock('hooks/useAuthZ/useAuthZ');
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
async function expandAllCards(): Promise<void> {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getResourcePanel } from '../../permissions.config';
|
||||
import ItemInputSelector from './ItemInputSelector';
|
||||
|
||||
import styles from './ActionToggle.module.scss';
|
||||
import { AuthZResource, AuthZVerb } from 'hooks/useAuthZ/types';
|
||||
import { AuthZResource, AuthZVerb } from 'lib/authz/hooks/useAuthZ/types';
|
||||
import { getActionLabel } from 'container/RolesSettings/ViewRolePage/components/permissionDisplay.utils';
|
||||
|
||||
const SCOPE_LABELS: Record<PermissionScope, string> = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ConfirmDialog } from '@signozhq/ui/dialog';
|
||||
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Skeleton } from 'antd';
|
||||
import type { AuthZResource, AuthZVerb } from 'hooks/useAuthZ/types';
|
||||
import type { AuthZResource, AuthZVerb } from 'lib/authz/hooks/useAuthZ/types';
|
||||
|
||||
import { PermissionScope, ResourcePermissions } from '../../types';
|
||||
import type { EditorMode, JsonEditorRef } from './JsonEditor.types';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
import type { AuthZResource, AuthZVerb } from 'hooks/useAuthZ/types';
|
||||
import type { AuthZResource, AuthZVerb } from 'lib/authz/hooks/useAuthZ/types';
|
||||
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Monaco } from '@monaco-editor/react';
|
||||
import permissionsType from 'hooks/useAuthZ/permissions.type';
|
||||
import permissionsType from 'lib/authz/hooks/useAuthZ/permissions.type';
|
||||
import transactionGroupSchema from 'schemas/generated/transactionGroups.schema.json';
|
||||
|
||||
export const TRANSACTION_GROUP_SCHEMA = transactionGroupSchema;
|
||||
|
||||
@@ -5,10 +5,10 @@ import { Pagination, Skeleton } from 'antd';
|
||||
import { useListRoles } from 'api/generated/services/role';
|
||||
import { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import PermissionDeniedFullPage from 'components/PermissionDeniedFullPage/PermissionDeniedFullPage';
|
||||
import PermissionDeniedFullPage from 'lib/authz/components/PermissionDeniedFullPage/PermissionDeniedFullPage';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { RoleListPermission } from 'hooks/useAuthZ/permissions/role.permissions';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { RoleListPermission } from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { useRolesFeatureGate } from 'hooks/useRolesFeatureGate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useHistory } from 'react-router-dom';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { RoleCreatePermission } from 'hooks/useAuthZ/permissions/role.permissions';
|
||||
import { RoleCreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
|
||||
import { useRolesFeatureGate } from 'hooks/useRolesFeatureGate';
|
||||
|
||||
import RolesListingTable from './RolesComponents/RolesListingTable';
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import { Skeleton } from 'antd';
|
||||
import { useGetRole } from 'api/generated/services/role';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import PermissionDeniedFullPage from 'components/PermissionDeniedFullPage/PermissionDeniedFullPage';
|
||||
import PermissionDeniedFullPage from 'lib/authz/components/PermissionDeniedFullPage/PermissionDeniedFullPage';
|
||||
import { useDeleteRoleModal } from 'container/RolesSettings/DeleteRoleModal/useDeleteRoleModal';
|
||||
import { useRoleAuthZ } from 'container/RolesSettings/hooks/useRoleAuthZ';
|
||||
import { transformApiToRolePermissions } from 'container/RolesSettings/hooks/useRolePermissions';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import * as useAuthZModule from 'hooks/useAuthZ/useAuthZ';
|
||||
import * as useAuthZModule from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import {
|
||||
customRoleResponse,
|
||||
managedRoleResponse,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
mockUseAuthZDenyAll,
|
||||
mockUseAuthZGrantAll,
|
||||
mockUseAuthZGrantByPrefix,
|
||||
} from 'tests/authz-test-utils';
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
import * as useRolePermissionsModule from '../../hooks/useRolePermissions';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import * as useAuthZModule from 'hooks/useAuthZ/useAuthZ';
|
||||
import * as useAuthZModule from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { customRoleResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
|
||||
import * as useRolePermissionsModule from '../../hooks/useRolePermissions';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import * as useAuthZModule from 'hooks/useAuthZ/useAuthZ';
|
||||
import * as useAuthZModule from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { customRoleResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
|
||||
import * as useRolePermissionsModule from '../../hooks/useRolePermissions';
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import * as useAuthZModule from 'hooks/useAuthZ/useAuthZ';
|
||||
import * as useAuthZModule from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { defaultFeatureFlags, render, screen } from 'tests/test-utils';
|
||||
import { invalidLicense, mockUseAuthZGrantAll } from 'tests/authz-test-utils';
|
||||
import {
|
||||
invalidLicense,
|
||||
mockUseAuthZGrantAll,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import ViewRolePage from '../ViewRolePage';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import * as useAuthZModule from 'hooks/useAuthZ/useAuthZ';
|
||||
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
|
||||
import * as useAuthZModule from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
import { render } from 'tests/test-utils';
|
||||
|
||||
import ViewRolePage from '../ViewRolePage';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import * as useAuthZModule from 'hooks/useAuthZ/useAuthZ';
|
||||
import * as useAuthZModule from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { customRoleResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen, within } from 'tests/test-utils';
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ import {
|
||||
CoretypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import * as useAuthZModule from 'hooks/useAuthZ/useAuthZ';
|
||||
import * as useAuthZModule from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import {
|
||||
customRoleResponse,
|
||||
managedRoleResponse,
|
||||
} from 'mocks-server/__mockdata__/roles';
|
||||
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
|
||||
import { mockUseAuthZGrantAll } from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import * as useRolePermissionsModule from '../../hooks/useRolePermissions';
|
||||
|
||||
|
||||
@@ -11,12 +11,15 @@ import {
|
||||
userEvent,
|
||||
} from 'tests/test-utils';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { invalidLicense, mockUseAuthZGrantAll } from 'tests/authz-test-utils';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import {
|
||||
invalidLicense,
|
||||
mockUseAuthZGrantAll,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import RolesSettings from '../RolesSettings';
|
||||
|
||||
jest.mock('hooks/useAuthZ/useAuthZ');
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
const rolesApiURL = 'http://localhost/api/v1/roles';
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
CoretypesKindDTO,
|
||||
CoretypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AuthZResource, AuthZVerb } from 'hooks/useAuthZ/types';
|
||||
import type { AuthZResource, AuthZVerb } from 'lib/authz/hooks/useAuthZ/types';
|
||||
|
||||
import {
|
||||
ActionConfig,
|
||||
|
||||
@@ -4,9 +4,12 @@ import {
|
||||
buildRoleReadPermission,
|
||||
buildRoleUpdatePermission,
|
||||
RoleCreatePermission,
|
||||
} from 'hooks/useAuthZ/permissions/role.permissions';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { ParsedPermissionObject, parsePermission } from 'hooks/useAuthZ/utils';
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import {
|
||||
ParsedPermissionObject,
|
||||
parsePermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/utils';
|
||||
|
||||
interface UseRoleAuthZResult {
|
||||
readRolePermission: ParsedPermissionObject;
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
useGetRole,
|
||||
useUpdateRole,
|
||||
} from 'api/generated/services/role';
|
||||
import type { AuthZResource, AuthZVerb } from 'hooks/useAuthZ/types';
|
||||
import type { AuthZResource, AuthZVerb } from 'lib/authz/hooks/useAuthZ/types';
|
||||
|
||||
import {
|
||||
getResourcePanel,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Bot, Key, Shield } from '@signozhq/icons';
|
||||
|
||||
import permissionsType from 'hooks/useAuthZ/permissions.type';
|
||||
import permissionsType from 'lib/authz/hooks/useAuthZ/permissions.type';
|
||||
import {
|
||||
AuthZResource,
|
||||
AuthZVerb,
|
||||
OBJECT_SCOPED_VERBS,
|
||||
ObjectScopedVerb,
|
||||
} from 'hooks/useAuthZ/types';
|
||||
} from 'lib/authz/hooks/useAuthZ/types';
|
||||
import { CoretypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
/** Shared shape of the icon components exported by `@signozhq/icons`. */
|
||||
@@ -84,7 +84,7 @@ export function getResourceVerbs(
|
||||
}
|
||||
|
||||
// Role resource cannot have assignee verb
|
||||
// TODO(H4ad): Remove this once we get rid of frontend/src/hooks/useAuthZ/legacy.ts
|
||||
// TODO(H4ad): Remove this once we get rid of frontend/lib/authz/hooks/useAuthZ/legacy.ts
|
||||
if (resource === 'role') {
|
||||
return match.allowedVerbs.filter((verb) => verb !== 'assignee');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AuthZResource, AuthZVerb } from 'hooks/useAuthZ/types';
|
||||
import type { AuthZResource, AuthZVerb } from 'lib/authz/hooks/useAuthZ/types';
|
||||
import { CoretypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export enum PermissionScope {
|
||||
|
||||
@@ -3,7 +3,10 @@ import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
import { AUTHZ_CHECK_URL, authzMockResponse } from 'tests/authz-test-utils';
|
||||
import {
|
||||
AUTHZ_CHECK_URL,
|
||||
authzMockResponse,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
import ServiceAccountsSettings from './ServiceAccountsSettings';
|
||||
|
||||
const SA_LIST_URL = 'http://localhost/api/v1/service_accounts';
|
||||
|
||||
@@ -4,10 +4,10 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { useListServiceAccounts } from 'api/generated/services/serviceaccount';
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import CreateServiceAccountModal from 'components/CreateServiceAccountModal/CreateServiceAccountModal';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import PermissionDeniedFullPage from 'components/PermissionDeniedFullPage/PermissionDeniedFullPage';
|
||||
import PermissionDeniedFullPage from 'lib/authz/components/PermissionDeniedFullPage/PermissionDeniedFullPage';
|
||||
import Spinner from 'components/Spinner';
|
||||
import ServiceAccountDrawer from 'components/ServiceAccountDrawer/ServiceAccountDrawer';
|
||||
import ServiceAccountsTable, {
|
||||
@@ -16,8 +16,8 @@ import ServiceAccountsTable, {
|
||||
import {
|
||||
SACreatePermission,
|
||||
SAListPermission,
|
||||
} from 'hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import {
|
||||
parseAsBoolean,
|
||||
parseAsInteger,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
import { setupAuthzAdmin } from 'tests/authz-test-utils';
|
||||
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import ServiceAccountsSettings from '../ServiceAccountsSettings';
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { buildPermission } from 'hooks/useAuthZ/utils';
|
||||
import type { AuthZObject, BrandedPermission } from 'hooks/useAuthZ/types';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { buildPermission } from 'lib/authz/hooks/useAuthZ/utils';
|
||||
import type {
|
||||
AuthZObject,
|
||||
BrandedPermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/types';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import AuthZTooltip from './AuthZTooltip';
|
||||
|
||||
jest.mock('hooks/useAuthZ/useAuthZ');
|
||||
jest.mock('lib/authz/hooks/useAuthZ/useAuthZ');
|
||||
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
|
||||
|
||||
const noPermissions = {
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@signozhq/ui/tooltip';
|
||||
import type { BrandedPermission } from 'hooks/useAuthZ/types';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { formatPermission } from 'hooks/useAuthZ/utils';
|
||||
import type { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { formatPermission } from 'lib/authz/hooks/useAuthZ/utils';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import styles from './AuthZTooltip.module.scss';
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { BrandedPermission } from 'hooks/useAuthZ/types';
|
||||
import { buildPermission } from 'hooks/useAuthZ/utils';
|
||||
import { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
|
||||
import { buildPermission } from 'lib/authz/hooks/useAuthZ/utils';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
import { AUTHZ_CHECK_URL, authzMockResponse } from 'tests/authz-test-utils';
|
||||
import {
|
||||
AUTHZ_CHECK_URL,
|
||||
authzMockResponse,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import { GuardAuthZ } from './GuardAuthZ';
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
AuthZObject,
|
||||
AuthZRelation,
|
||||
BrandedPermission,
|
||||
} from 'hooks/useAuthZ/types';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { buildPermission } from 'hooks/useAuthZ/utils';
|
||||
} from 'lib/authz/hooks/useAuthZ/types';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { buildPermission } from 'lib/authz/hooks/useAuthZ/utils';
|
||||
|
||||
export type GuardAuthZProps<R extends AuthZRelation> = {
|
||||
children: ReactElement;
|
||||
@@ -7,7 +7,10 @@ import type {
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
import { AUTHZ_CHECK_URL, authzMockResponse } from 'tests/authz-test-utils';
|
||||
import {
|
||||
AUTHZ_CHECK_URL,
|
||||
authzMockResponse,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import { createGuardedRoute } from './createGuardedRoute';
|
||||
|
||||
@@ -4,13 +4,13 @@ import {
|
||||
AuthZObject,
|
||||
AuthZRelation,
|
||||
BrandedPermission,
|
||||
} from 'hooks/useAuthZ/types';
|
||||
import { formatPermission } from 'hooks/useAuthZ/utils';
|
||||
} from 'lib/authz/hooks/useAuthZ/types';
|
||||
import { formatPermission } from 'lib/authz/hooks/useAuthZ/utils';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import noDataUrl from '@/assets/Icons/no-data.svg';
|
||||
import noDataUrl from 'assets/Icons/no-data.svg';
|
||||
|
||||
import AppLoading from '../AppLoading/AppLoading';
|
||||
import AppLoading from '../../../../components/AppLoading/AppLoading';
|
||||
import { GuardAuthZ } from '../GuardAuthZ/GuardAuthZ';
|
||||
|
||||
import './createGuardedRoute.styles.scss';
|
||||
@@ -3,7 +3,10 @@ import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { AllTheProviders } from 'tests/test-utils';
|
||||
import { AUTHZ_CHECK_URL, authzMockResponse } from 'tests/authz-test-utils';
|
||||
import {
|
||||
AUTHZ_CHECK_URL,
|
||||
authzMockResponse,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
|
||||
import { BrandedPermission } from './types';
|
||||
import { useAuthZ } from './useAuthZ';
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
CoretypesTypeDTO,
|
||||
AuthtypesRelationDTO,
|
||||
CoretypesKindDTO,
|
||||
} from '../../api/generated/services/sigNoz.schemas';
|
||||
} from '../../../../api/generated/services/sigNoz.schemas';
|
||||
import permissionsType from './permissions.type';
|
||||
import {
|
||||
AuthZObject,
|
||||
@@ -3,12 +3,12 @@ import type {
|
||||
AuthtypesTransactionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { gettableTransactionToPermission } from 'hooks/useAuthZ/utils';
|
||||
import { gettableTransactionToPermission } from 'lib/authz/hooks/useAuthZ/utils';
|
||||
import type {
|
||||
BrandedPermission,
|
||||
UseAuthZOptions,
|
||||
UseAuthZResult,
|
||||
} from 'hooks/useAuthZ/types';
|
||||
} from 'lib/authz/hooks/useAuthZ/types';
|
||||
import { rest } from 'msw';
|
||||
import type { RestHandler } from 'msw';
|
||||
import {
|
||||
@@ -4,6 +4,7 @@ import { toast } from '@signozhq/ui/sonner';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
lockDashboardV2,
|
||||
patchDashboardV2,
|
||||
unlockDashboardV2,
|
||||
} from 'api/generated/services/dashboard';
|
||||
import type {
|
||||
@@ -17,7 +18,6 @@ import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { useCreatePanel } from '../hooks/useCreatePanel';
|
||||
import { useOptimisticPatch } from '../hooks/useOptimisticPatch';
|
||||
import PanelTypeSelectionModal from '../PanelsAndSectionsLayout/Panel/PanelTypeSelectionModal/PanelTypeSelectionModal';
|
||||
import DashboardActions from './DashboardActions/DashboardActions';
|
||||
import DashboardInfo from './DashboardInfo/DashboardInfo';
|
||||
@@ -51,7 +51,6 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
|
||||
|
||||
const { user } = useAppContext();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const { patchAsync } = useOptimisticPatch();
|
||||
const { isPickerOpen, openPicker, closePicker, createPanel } =
|
||||
useCreatePanel();
|
||||
|
||||
@@ -89,13 +88,14 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
|
||||
value: next,
|
||||
},
|
||||
];
|
||||
await patchAsync(patch);
|
||||
await patchDashboardV2({ id }, patch);
|
||||
toast.success('Dashboard renamed successfully');
|
||||
refetch();
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
}
|
||||
},
|
||||
[id, patchAsync, showErrorModal],
|
||||
[id, refetch, showErrorModal],
|
||||
);
|
||||
|
||||
const { isEditing, draft, setDraft, startEdit, cancel, commit } =
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { patchDashboardV2 } from 'api/generated/services/dashboard';
|
||||
import type {
|
||||
DashboardtypesGettableDashboardV2DTO,
|
||||
DashboardtypesJSONPatchOperationDTO,
|
||||
@@ -8,7 +9,7 @@ import { isEqual } from 'lodash-es';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { useOptimisticPatch } from '../../hooks/useOptimisticPatch';
|
||||
import { useDashboardStore } from '../../store/useDashboardStore';
|
||||
import CrossPanelSync from './CrossPanelSync/CrossPanelSync';
|
||||
import DashboardInfoForm from './DashboardInfoForm/DashboardInfoForm';
|
||||
import UnsavedChangesFooter from './UnsavedChangesFooter/UnsavedChangesFooter';
|
||||
@@ -22,7 +23,7 @@ interface OverviewProps {
|
||||
function Overview({ dashboard }: OverviewProps): JSX.Element {
|
||||
const id = dashboard.id;
|
||||
|
||||
const { patchAsync } = useOptimisticPatch();
|
||||
const refetch = useDashboardStore((s) => s.refetch);
|
||||
|
||||
const title = dashboard.spec.display.name;
|
||||
const description = dashboard.spec.display.description ?? '';
|
||||
@@ -95,14 +96,15 @@ function Overview({ dashboard }: OverviewProps): JSX.Element {
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
await patchAsync(ops);
|
||||
await patchDashboardV2({ id }, ops);
|
||||
toast.success('Dashboard updated');
|
||||
refetch();
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [buildPatch, patchAsync, showErrorModal]);
|
||||
}, [id, buildPatch, refetch, showErrorModal]);
|
||||
|
||||
useEffect(() => {
|
||||
let numberOfUnsavedChanges = 0;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { patchDashboardV2 } from 'api/generated/services/dashboard';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { useOptimisticPatch } from '../../hooks/useOptimisticPatch';
|
||||
import { useDashboardStore } from '../../store/useDashboardStore';
|
||||
import { formModelToDto } from './variableAdapters';
|
||||
import type { VariableFormModel } from './variableFormModel';
|
||||
@@ -14,9 +14,14 @@ interface UseSaveVariables {
|
||||
isSaving: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the dashboard's variable list via a single `/spec/variables` patch,
|
||||
* then refetches. Mirrors the General-settings save flow (patch → toast →
|
||||
* refetch → surface errors).
|
||||
*/
|
||||
export function useSaveVariables(): UseSaveVariables {
|
||||
const dashboardId = useDashboardStore((s) => s.dashboardId);
|
||||
const { patchAsync } = useOptimisticPatch();
|
||||
const refetch = useDashboardStore((s) => s.refetch);
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
@@ -28,8 +33,9 @@ export function useSaveVariables(): UseSaveVariables {
|
||||
const dtos = variables.map(formModelToDto);
|
||||
try {
|
||||
setIsSaving(true);
|
||||
await patchAsync(buildVariablesPatch(dtos));
|
||||
await patchDashboardV2({ id: dashboardId }, buildVariablesPatch(dtos));
|
||||
toast.success('Variables updated');
|
||||
refetch();
|
||||
return true;
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
@@ -38,7 +44,7 @@ export function useSaveVariables(): UseSaveVariables {
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[dashboardId, patchAsync, showErrorModal],
|
||||
[dashboardId, refetch, showErrorModal],
|
||||
);
|
||||
|
||||
return { save, isSaving };
|
||||
|
||||
@@ -40,8 +40,6 @@ interface ConfigPaneProps {
|
||||
*/
|
||||
panel: DashboardtypesPanelDTO;
|
||||
panelId: string;
|
||||
/** Unit the selected metric was sent with; drives the unit selector's mismatch warning. */
|
||||
metricUnit?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,7 +58,6 @@ function ConfigPane({
|
||||
stepInterval,
|
||||
panel,
|
||||
panelId,
|
||||
metricUnit,
|
||||
}: ConfigPaneProps): JSX.Element {
|
||||
const panelKind = spec.plugin.kind;
|
||||
const definition = getPanelDefinition(panelKind);
|
||||
@@ -121,7 +118,6 @@ function ConfigPane({
|
||||
onChangePanelKind={onChangePanelKind}
|
||||
queryType={queryType}
|
||||
stepInterval={stepInterval}
|
||||
metricUnit={metricUnit}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,6 @@ function SectionSlot({
|
||||
onChangePanelKind,
|
||||
queryType,
|
||||
stepInterval,
|
||||
metricUnit,
|
||||
}: SectionSlotProps): JSX.Element | null {
|
||||
// A kind can hide a section based on current spec state (e.g. Histogram legend once
|
||||
// queries are merged) — skip it before resolving the editor.
|
||||
@@ -75,7 +74,6 @@ function SectionSlot({
|
||||
onChangePanelKind={onChangePanelKind}
|
||||
queryType={queryType}
|
||||
stepInterval={stepInterval}
|
||||
metricUnit={metricUnit}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,4 @@ export interface SectionEditorContext {
|
||||
yAxisUnit?: string;
|
||||
queryType?: EQueryType;
|
||||
stepInterval?: number;
|
||||
/** Unit the selected metric was sent with; drives the unit selector's mismatch warning. */
|
||||
metricUnit?: string;
|
||||
}
|
||||
|
||||
@@ -46,10 +46,11 @@ function DisconnectValuesField({
|
||||
onChange,
|
||||
}: DisconnectValuesFieldProps): JSX.Element {
|
||||
const duration = value?.fillLessThan || undefined;
|
||||
// `fillOnlyBelow` is authoritative; fall back to a stored duration for legacy panels.
|
||||
const isThreshold = value?.fillOnlyBelow ?? !!duration;
|
||||
// Remember the last committed threshold so Never → Threshold restores it.
|
||||
const [lastDuration, setLastDuration] = useState<string | undefined>(duration);
|
||||
const isThreshold = !!duration;
|
||||
// Remember the last threshold so toggling Never → Threshold restores it.
|
||||
const [lastDuration, setLastDuration] = useState(
|
||||
duration ?? defaultDuration(stepInterval),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (duration) {
|
||||
@@ -58,17 +59,11 @@ function DisconnectValuesField({
|
||||
}, [duration]);
|
||||
|
||||
const handleMode = (mode: DisconnectValuesMode): void => {
|
||||
if (mode === DisconnectValuesMode.THRESHOLD) {
|
||||
onChange({
|
||||
...value,
|
||||
fillOnlyBelow: true,
|
||||
// Seed from the live stepInterval (async — undefined until results load), not mount.
|
||||
fillLessThan: lastDuration ?? defaultDuration(stepInterval),
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Never spans every gap; drop the duration so the renderer reads a clean "span all".
|
||||
onChange({ ...value, fillOnlyBelow: false, fillLessThan: undefined });
|
||||
onChange(
|
||||
mode === DisconnectValuesMode.THRESHOLD
|
||||
? { ...value, fillLessThan: lastDuration }
|
||||
: undefined,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -84,16 +79,14 @@ function DisconnectValuesField({
|
||||
onChange={handleMode}
|
||||
/>
|
||||
</div>
|
||||
{isThreshold && duration && (
|
||||
{isThreshold && (
|
||||
<div className={styles.field}>
|
||||
<Typography.Text>Threshold value</Typography.Text>
|
||||
<DisconnectValuesThresholdInput
|
||||
testId={`${testId}-value`}
|
||||
value={duration}
|
||||
value={lastDuration}
|
||||
minValue={stepInterval}
|
||||
onChange={(next): void =>
|
||||
onChange({ ...value, fillOnlyBelow: true, fillLessThan: next })
|
||||
}
|
||||
onChange={(next): void => onChange({ ...value, fillLessThan: next })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -14,28 +14,6 @@ interface DisconnectValuesThresholdInputProps {
|
||||
onChange: (duration: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline error for a raw duration, or `null` when valid and in range. The parse is
|
||||
* guarded: `isValidTimeSpan` passes some strings `intervalToSeconds` throws on (e.g. "5x").
|
||||
*/
|
||||
function validationError(raw: string, minValue?: number): string | null {
|
||||
let seconds: number;
|
||||
try {
|
||||
seconds = rangeUtil.isValidTimeSpan(raw)
|
||||
? rangeUtil.intervalToSeconds(raw)
|
||||
: NaN;
|
||||
} catch {
|
||||
seconds = NaN;
|
||||
}
|
||||
if (!Number.isFinite(seconds) || seconds <= 0) {
|
||||
return 'Enter a valid duration (e.g. 30s, 1m, 1h)';
|
||||
}
|
||||
if (minValue !== undefined && seconds < minValue) {
|
||||
return `Threshold should be > ${rangeUtil.secondsToHms(minValue)}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duration input for the span-gaps threshold: shows/accepts and reports a human
|
||||
* duration ("30s", "1m", "1h"), which is the value stored verbatim in
|
||||
@@ -58,21 +36,24 @@ function DisconnectValuesThresholdInput({
|
||||
setError(null);
|
||||
}, [value]);
|
||||
|
||||
// Validate live so an invalid entry surfaces immediately, not only on blur.
|
||||
const handleText = (raw: string): void => {
|
||||
setText(raw);
|
||||
setError(raw ? validationError(raw, minValue) : null);
|
||||
};
|
||||
|
||||
const commit = (raw: string): void => {
|
||||
// Skip no-op commits: blur fires when clicking the Never toggle, and re-emitting
|
||||
// the unchanged value there would race the toggle and snap back to Threshold.
|
||||
if (!raw || raw === value) {
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
const message = validationError(raw, minValue);
|
||||
if (message) {
|
||||
setError(message);
|
||||
let seconds: number;
|
||||
try {
|
||||
seconds = rangeUtil.isValidTimeSpan(raw)
|
||||
? rangeUtil.intervalToSeconds(raw)
|
||||
: NaN;
|
||||
} catch {
|
||||
seconds = NaN;
|
||||
}
|
||||
if (!Number.isFinite(seconds) || seconds <= 0) {
|
||||
setError('Enter a valid duration (e.g. 30s, 1m, 1h)');
|
||||
return;
|
||||
}
|
||||
if (minValue !== undefined && seconds < minValue) {
|
||||
setError(`Threshold should be > ${rangeUtil.secondsToHms(minValue)}`);
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
@@ -88,9 +69,12 @@ function DisconnectValuesThresholdInput({
|
||||
status={error ? 'error' : undefined}
|
||||
prefix={<span className={styles.thresholdPrefix}>></span>}
|
||||
value={text}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
|
||||
handleText(e.target.value)
|
||||
}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setText(e.target.value);
|
||||
if (error) {
|
||||
setError(null);
|
||||
}
|
||||
}}
|
||||
onBlur={(e): void => commit(e.currentTarget.value)}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
|
||||
@@ -1,34 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
DashboardtypesLineStyleDTO,
|
||||
type DashboardtypesTimeSeriesChartAppearanceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { DashboardtypesLineStyleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import ChartAppearanceSection from '../ChartAppearanceSection';
|
||||
|
||||
/** Stateful wrapper that feeds onChange back as the spec, mirroring the real editor. */
|
||||
function StatefulSpanGaps({
|
||||
initial,
|
||||
stepInterval,
|
||||
}: {
|
||||
initial?: DashboardtypesTimeSeriesChartAppearanceDTO;
|
||||
stepInterval?: number;
|
||||
}): JSX.Element {
|
||||
const [value, setValue] = useState<
|
||||
DashboardtypesTimeSeriesChartAppearanceDTO | undefined
|
||||
>(initial);
|
||||
return (
|
||||
<ChartAppearanceSection
|
||||
value={value}
|
||||
controls={{ spanGaps: true }}
|
||||
stepInterval={stepInterval}
|
||||
onChange={setValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Open the antd Select by clicking its selector, then pick the option by label. The
|
||||
// line-style and fill-mode controls are ConfigSegmented (buttons), so this helper is
|
||||
// only used for the line-interpolation ConfigSelect.
|
||||
@@ -164,7 +139,7 @@ describe('ChartAppearanceSection', () => {
|
||||
await user.click(screen.getByText('Threshold'));
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
spanGaps: { fillOnlyBelow: true, fillLessThan: '1m' },
|
||||
spanGaps: { fillLessThan: '1m' },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -187,7 +162,7 @@ describe('ChartAppearanceSection', () => {
|
||||
await user.tab();
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
spanGaps: { fillOnlyBelow: true, fillLessThan: '5m' },
|
||||
spanGaps: { fillLessThan: '5m' },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,7 +183,7 @@ describe('ChartAppearanceSection', () => {
|
||||
await user.tab();
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
spanGaps: { fillOnlyBelow: true, fillLessThan: '300' },
|
||||
spanGaps: { fillLessThan: '300' },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -225,24 +200,7 @@ describe('ChartAppearanceSection', () => {
|
||||
|
||||
await user.click(screen.getByText('Never'));
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
spanGaps: { fillOnlyBelow: false, fillLessThan: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('selects Never when fillOnlyBelow is false even if a duration lingers', () => {
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
value={{ spanGaps: { fillOnlyBelow: false, fillLessThan: '1m' } }}
|
||||
controls={{ spanGaps: true }}
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
// The flag is authoritative: a stale fillLessThan must not show Threshold.
|
||||
expect(
|
||||
screen.queryByTestId('panel-editor-v2-span-gaps-value'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(onChange).toHaveBeenLastCalledWith({ spanGaps: undefined });
|
||||
});
|
||||
|
||||
it('shows an error and does not commit an invalid duration', async () => {
|
||||
@@ -286,117 +244,4 @@ describe('ChartAppearanceSection', () => {
|
||||
expect(screen.getByText(/Threshold should be >/)).toBeInTheDocument();
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('seeds the threshold from the step interval when switching to Threshold', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
value={undefined}
|
||||
controls={{ spanGaps: true }}
|
||||
stepInterval={300}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('Threshold'));
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
spanGaps: { fillOnlyBelow: true, fillLessThan: '5m' },
|
||||
});
|
||||
});
|
||||
|
||||
it('seeds from the step interval even when it arrives after mount', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
// The step interval is undefined until the query response carries step metadata,
|
||||
// so the panel first renders without it and receives it on a later render.
|
||||
const { rerender } = render(
|
||||
<ChartAppearanceSection
|
||||
value={undefined}
|
||||
controls={{ spanGaps: true }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
rerender(
|
||||
<ChartAppearanceSection
|
||||
value={undefined}
|
||||
controls={{ spanGaps: true }}
|
||||
stepInterval={300}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('Threshold'));
|
||||
|
||||
// Regression: a value seeded at mount would still be the 1m fallback.
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
spanGaps: { fillOnlyBelow: true, fillLessThan: '5m' },
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a validation error while typing, before blur', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
value={{ spanGaps: { fillLessThan: '1m' } }}
|
||||
controls={{ spanGaps: true }}
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByTestId('panel-editor-v2-span-gaps-value');
|
||||
await user.clear(input);
|
||||
await user.type(input, 'abc');
|
||||
// No blur / Enter — the error must already be visible.
|
||||
|
||||
expect(screen.getByText(/valid duration/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not re-commit the threshold when blurred without a change', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
value={{ spanGaps: { fillLessThan: '1m' } }}
|
||||
controls={{ spanGaps: true }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByTestId('panel-editor-v2-span-gaps-value');
|
||||
await user.click(input);
|
||||
await user.tab();
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fully switches from Threshold to Never (the input disappears)', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StatefulSpanGaps initial={{ spanGaps: { fillLessThan: '1m' } }} />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId('panel-editor-v2-span-gaps-value'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Focus the input first so clicking Never also fires its blur (the toggle race).
|
||||
await user.click(screen.getByTestId('panel-editor-v2-span-gaps-value'));
|
||||
await user.click(screen.getByText('Never'));
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('panel-editor-v2-span-gaps-value'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('remembers the last threshold when toggling Never → Threshold', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StatefulSpanGaps initial={{ spanGaps: { fillLessThan: '5m' } }} />);
|
||||
|
||||
await user.click(screen.getByText('Never'));
|
||||
await user.click(screen.getByText('Threshold'));
|
||||
|
||||
expect(screen.getByTestId('panel-editor-v2-span-gaps-value')).toHaveValue(
|
||||
'5m',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ import ColumnUnits from './ColumnUnits';
|
||||
import styles from './FormattingSection.module.scss';
|
||||
|
||||
type FormattingSectionProps = SectionEditorProps<SectionKind.Formatting> &
|
||||
Pick<SectionEditorContext, 'tableColumns' | 'metricUnit'>;
|
||||
Pick<SectionEditorContext, 'tableColumns'>;
|
||||
|
||||
// `full` means "show the raw value, no rounding"; the digits round to that many places.
|
||||
const DECIMAL_OPTIONS: {
|
||||
@@ -39,7 +39,6 @@ function FormattingSection({
|
||||
controls,
|
||||
onChange,
|
||||
tableColumns = [],
|
||||
metricUnit,
|
||||
}: FormattingSectionProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
@@ -51,7 +50,6 @@ function FormattingSection({
|
||||
data-testid="panel-editor-v2-unit"
|
||||
source={YAxisSource.DASHBOARDS}
|
||||
value={value?.unit}
|
||||
initialValue={metricUnit}
|
||||
onChange={(unit): void => onChange({ ...value, unit })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,6 @@ import userEvent from '@testing-library/user-event';
|
||||
|
||||
import FormattingSection from '../FormattingSection';
|
||||
|
||||
// Auto-seeding is covered by useMetricYAxisUnit's tests; here `metricUnit` is just a prop.
|
||||
|
||||
// Open the Decimals select (clicking its antd selector) and pick the option with the
|
||||
// given visible label.
|
||||
async function pickDecimal(label: string): Promise<void> {
|
||||
@@ -73,31 +71,4 @@ describe('FormattingSection', () => {
|
||||
decimalPrecision: '2',
|
||||
});
|
||||
});
|
||||
|
||||
it('warns when the selected unit mismatches the metric unit', () => {
|
||||
// metric sent in seconds, but bytes is selected.
|
||||
render(
|
||||
<FormattingSection
|
||||
value={{ unit: 'By' }}
|
||||
controls={{ unit: true }}
|
||||
metricUnit="s"
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText('warning')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows no warning when the selected unit matches the metric unit', () => {
|
||||
render(
|
||||
<FormattingSection
|
||||
value={{ unit: 's' }}
|
||||
controls={{ unit: true }}
|
||||
metricUnit="s"
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText('warning')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import {
|
||||
@@ -82,15 +82,6 @@ function ThresholdsSection({
|
||||
// Which row is being edited, and whether it was just added (so Discard removes it).
|
||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||
const [unsavedIndex, setUnsavedIndex] = useState<number | null>(null);
|
||||
// The saved threshold captured on edit entry, restored if the edit is discarded
|
||||
// (edits stream into the spec live, so Discard can't just drop a local draft).
|
||||
const editSnapshot = useRef<AnyThreshold | null>(null);
|
||||
|
||||
const updateAt =
|
||||
(index: number) =>
|
||||
(next: AnyThreshold): void => {
|
||||
onChange(thresholds.map((t, i) => (i === index ? next : t)));
|
||||
};
|
||||
|
||||
const addThreshold = (): void => {
|
||||
const nextIndex = thresholds.length;
|
||||
@@ -99,11 +90,6 @@ function ThresholdsSection({
|
||||
setUnsavedIndex(nextIndex);
|
||||
};
|
||||
|
||||
const beginEdit = (index: number): void => {
|
||||
editSnapshot.current = thresholds[index] ?? null;
|
||||
setEditingIndex(index);
|
||||
};
|
||||
|
||||
const saveAt =
|
||||
(index: number) =>
|
||||
(next: AnyThreshold): void => {
|
||||
@@ -119,15 +105,11 @@ function ThresholdsSection({
|
||||
};
|
||||
|
||||
const discardAt = (index: number) => (): void => {
|
||||
// A never-saved row is removed; otherwise revert the live edits to the snapshot.
|
||||
// Discarding a row that was never saved removes it; otherwise just exit edit.
|
||||
if (index === unsavedIndex) {
|
||||
removeAt(index);
|
||||
return;
|
||||
}
|
||||
const original = editSnapshot.current;
|
||||
if (original) {
|
||||
onChange(thresholds.map((t, i) => (i === index ? original : t)));
|
||||
}
|
||||
setEditingIndex(null);
|
||||
};
|
||||
|
||||
@@ -138,9 +120,8 @@ function ThresholdsSection({
|
||||
index,
|
||||
yAxisUnit,
|
||||
isEditing: editingIndex === index,
|
||||
onEdit: (): void => beginEdit(index),
|
||||
onEdit: (): void => setEditingIndex(index),
|
||||
onSave: saveAt(index),
|
||||
onLiveChange: updateAt(index),
|
||||
onDiscard: discardAt(index),
|
||||
onRemove: (): void => removeAt(index),
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
DashboardtypesThresholdFormatDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AnyThreshold } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import UnifiedThresholdsSection from '../ThresholdsSection';
|
||||
|
||||
@@ -36,16 +36,9 @@ const THRESHOLDS: DashboardtypesComparisonThresholdDTO[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// Stateful harness for flows that depend on the value updating (add/discard/live).
|
||||
function Harness({
|
||||
yAxisUnit,
|
||||
initial = [],
|
||||
}: {
|
||||
yAxisUnit?: string;
|
||||
initial?: DashboardtypesComparisonThresholdDTO[];
|
||||
}): JSX.Element {
|
||||
const [value, setValue] =
|
||||
useState<DashboardtypesComparisonThresholdDTO[]>(initial);
|
||||
// Stateful harness for flows that depend on the value updating (add/discard).
|
||||
function Harness({ yAxisUnit }: { yAxisUnit?: string }): JSX.Element {
|
||||
const [value, setValue] = useState<DashboardtypesComparisonThresholdDTO[]>([]);
|
||||
return (
|
||||
<ComparisonThresholdsSection
|
||||
value={value}
|
||||
@@ -149,46 +142,24 @@ describe('ComparisonThresholdsSection', () => {
|
||||
expect(valueInput).toHaveValue(5);
|
||||
});
|
||||
|
||||
it('reflects edits live (before Save) so the preview can react', async () => {
|
||||
it('does not commit edits when Discard is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<ComparisonThresholdsSection value={THRESHOLDS} onChange={onChange} />,
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId('comparison-threshold-edit-0'));
|
||||
await user.clear(screen.getByTestId('comparison-threshold-value-0'));
|
||||
await user.type(screen.getByTestId('comparison-threshold-value-0'), '90');
|
||||
|
||||
// No Save click — the latest edit is pushed up (debounced) for the preview.
|
||||
await waitFor(() =>
|
||||
expect(onChange).toHaveBeenLastCalledWith([
|
||||
{
|
||||
value: 90,
|
||||
color: '#F5B225',
|
||||
operator: DashboardtypesComparisonOperatorDTO.above,
|
||||
unit: 'percent',
|
||||
format: DashboardtypesThresholdFormatDTO.background,
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts the live edits to the saved value on Discard', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Harness initial={THRESHOLDS} />);
|
||||
|
||||
await user.click(screen.getByTestId('comparison-threshold-edit-0'));
|
||||
await user.clear(screen.getByTestId('comparison-threshold-value-0'));
|
||||
await user.type(screen.getByTestId('comparison-threshold-value-0'), '90');
|
||||
await user.click(screen.getByTestId('comparison-threshold-discard-0'));
|
||||
|
||||
// Back to view mode, and re-opening shows the rolled-back 80, not 90.
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
// Back to view mode.
|
||||
expect(
|
||||
screen.queryByTestId('comparison-threshold-value-0'),
|
||||
).not.toBeInTheDocument();
|
||||
await user.click(screen.getByTestId('comparison-threshold-edit-0'));
|
||||
expect(screen.getByTestId('comparison-threshold-value-0')).toHaveValue(80);
|
||||
expect(screen.getByTestId('comparison-threshold-edit-0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('removes a threshold from view mode', async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { DashboardtypesThresholdWithLabelDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AnyThreshold } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
@@ -10,16 +10,10 @@ const THRESHOLDS: DashboardtypesThresholdWithLabelDTO[] = [
|
||||
{ value: 80, color: '#F5B225', label: 'High', unit: 'percent' },
|
||||
];
|
||||
|
||||
// Stateful harness for flows that depend on the value updating (add/discard/live);
|
||||
// Stateful harness for flows that depend on the value updating (add/discard);
|
||||
// omits `controls` to exercise the default `label` variant.
|
||||
function Harness({
|
||||
yAxisUnit,
|
||||
initial = [],
|
||||
}: {
|
||||
yAxisUnit?: string;
|
||||
initial?: AnyThreshold[];
|
||||
}): JSX.Element {
|
||||
const [value, setValue] = useState<AnyThreshold[]>(initial);
|
||||
function Harness({ yAxisUnit }: { yAxisUnit?: string }): JSX.Element {
|
||||
const [value, setValue] = useState<AnyThreshold[]>([]);
|
||||
return (
|
||||
<ThresholdsSection value={value} onChange={setValue} yAxisUnit={yAxisUnit} />
|
||||
);
|
||||
@@ -43,20 +37,19 @@ describe('ThresholdsSection', () => {
|
||||
expect(screen.queryByTestId('threshold-value-0')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('edits a threshold value and commits it on Save', async () => {
|
||||
const user = userEvent.setup();
|
||||
it('edits a threshold value and commits it on Save', () => {
|
||||
const onChange = jest.fn();
|
||||
render(<ThresholdsSection value={THRESHOLDS} onChange={onChange} />);
|
||||
|
||||
await user.click(screen.getByTestId('threshold-edit-0'));
|
||||
const valueInput = screen.getByTestId('threshold-value-0');
|
||||
expect(valueInput).toHaveValue(80);
|
||||
fireEvent.click(screen.getByTestId('threshold-edit-0'));
|
||||
expect(screen.getByTestId('threshold-value-0')).toHaveValue(80);
|
||||
|
||||
await user.clear(valueInput);
|
||||
await user.type(valueInput, '90');
|
||||
await user.click(screen.getByTestId('threshold-save-0'));
|
||||
fireEvent.change(screen.getByTestId('threshold-value-0'), {
|
||||
target: { value: '90' },
|
||||
});
|
||||
fireEvent.click(screen.getByTestId('threshold-save-0'));
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith([
|
||||
expect(onChange).toHaveBeenCalledWith([
|
||||
{ value: 90, color: '#F5B225', label: 'High', unit: 'percent' },
|
||||
]);
|
||||
});
|
||||
@@ -77,63 +70,43 @@ describe('ThresholdsSection', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('reflects edits live (before Save) so the preview can react', async () => {
|
||||
const user = userEvent.setup();
|
||||
it('does not commit edits when Discard is clicked', () => {
|
||||
const onChange = jest.fn();
|
||||
render(<ThresholdsSection value={THRESHOLDS} onChange={onChange} />);
|
||||
|
||||
await user.click(screen.getByTestId('threshold-edit-0'));
|
||||
await user.clear(screen.getByTestId('threshold-value-0'));
|
||||
await user.type(screen.getByTestId('threshold-value-0'), '90');
|
||||
fireEvent.click(screen.getByTestId('threshold-edit-0'));
|
||||
fireEvent.change(screen.getByTestId('threshold-value-0'), {
|
||||
target: { value: '90' },
|
||||
});
|
||||
fireEvent.click(screen.getByTestId('threshold-discard-0'));
|
||||
|
||||
// No Save click — the edit is pushed up (debounced) for the preview to render.
|
||||
await waitFor(() =>
|
||||
expect(onChange).toHaveBeenLastCalledWith([
|
||||
{ value: 90, color: '#F5B225', label: 'High', unit: 'percent' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts the live edits to the saved value on Discard', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Harness initial={THRESHOLDS} />);
|
||||
|
||||
await user.click(screen.getByTestId('threshold-edit-0'));
|
||||
await user.clear(screen.getByTestId('threshold-value-0'));
|
||||
await user.type(screen.getByTestId('threshold-value-0'), '90');
|
||||
await user.click(screen.getByTestId('threshold-discard-0'));
|
||||
|
||||
// Back to view mode, and re-opening shows the rolled-back 80, not 90.
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
expect(screen.queryByTestId('threshold-value-0')).not.toBeInTheDocument();
|
||||
await user.click(screen.getByTestId('threshold-edit-0'));
|
||||
expect(screen.getByTestId('threshold-value-0')).toHaveValue(80);
|
||||
expect(screen.getByTestId('threshold-edit-0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('removes a threshold from view mode', async () => {
|
||||
const user = userEvent.setup();
|
||||
it('removes a threshold from view mode', () => {
|
||||
const onChange = jest.fn();
|
||||
render(<ThresholdsSection value={THRESHOLDS} onChange={onChange} />);
|
||||
|
||||
await user.click(screen.getByTestId('threshold-remove-0'));
|
||||
fireEvent.click(screen.getByTestId('threshold-remove-0'));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('adds a threshold that opens in edit mode, and discards it away', async () => {
|
||||
const user = userEvent.setup();
|
||||
it('adds a threshold that opens in edit mode, and discards it away', () => {
|
||||
render(<Harness />);
|
||||
|
||||
await user.click(screen.getByTestId('panel-editor-v2-add-threshold'));
|
||||
fireEvent.click(screen.getByTestId('panel-editor-v2-add-threshold'));
|
||||
expect(screen.getByTestId('threshold-value-0')).toBeInTheDocument();
|
||||
|
||||
// Discarding a never-saved row removes it entirely.
|
||||
await user.click(screen.getByTestId('threshold-discard-0'));
|
||||
fireEvent.click(screen.getByTestId('threshold-discard-0'));
|
||||
expect(screen.queryByTestId('threshold-value-0')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('threshold-edit-0')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('flags a threshold unit in a different category than the y-axis unit', async () => {
|
||||
const user = userEvent.setup();
|
||||
it('flags a threshold unit in a different category than the y-axis unit', () => {
|
||||
render(
|
||||
<ThresholdsSection
|
||||
value={[{ value: 80, color: '#F5B225', label: '', unit: 'ms' }]}
|
||||
@@ -142,12 +115,11 @@ describe('ThresholdsSection', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId('threshold-edit-0'));
|
||||
fireEvent.click(screen.getByTestId('threshold-edit-0'));
|
||||
expect(screen.getByTestId('threshold-unit-invalid-0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not flag a threshold unit in the same category as the y-axis unit', async () => {
|
||||
const user = userEvent.setup();
|
||||
it('does not flag a threshold unit in the same category as the y-axis unit', () => {
|
||||
render(
|
||||
<ThresholdsSection
|
||||
value={[{ value: 80, color: '#F5B225', label: '', unit: 'ms' }]}
|
||||
@@ -156,7 +128,7 @@ describe('ThresholdsSection', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId('threshold-edit-0'));
|
||||
fireEvent.click(screen.getByTestId('threshold-edit-0'));
|
||||
expect(
|
||||
screen.queryByTestId('threshold-unit-invalid-0'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
@@ -27,7 +27,6 @@ interface ComparisonThresholdRowProps {
|
||||
isEditing: boolean;
|
||||
onEdit: () => void;
|
||||
onSave: (next: DashboardtypesComparisonThresholdDTO) => void;
|
||||
onLiveChange: (next: DashboardtypesComparisonThresholdDTO) => void;
|
||||
onDiscard: () => void;
|
||||
onRemove: () => void;
|
||||
}
|
||||
@@ -43,15 +42,10 @@ function ComparisonThresholdRow({
|
||||
isEditing,
|
||||
onEdit,
|
||||
onSave,
|
||||
onLiveChange,
|
||||
onDiscard,
|
||||
onRemove,
|
||||
}: ComparisonThresholdRowProps): JSX.Element {
|
||||
const { draft, setDraft, setValue } = useThresholdDraft(
|
||||
threshold,
|
||||
isEditing,
|
||||
onLiveChange,
|
||||
);
|
||||
const { draft, setDraft, setValue } = useThresholdDraft(threshold, isEditing);
|
||||
|
||||
const symbol = threshold.operator ? OPERATOR_SYMBOL[threshold.operator] : '';
|
||||
const summary = (
|
||||
|
||||
@@ -20,7 +20,6 @@ interface LabelThresholdRowProps {
|
||||
isEditing: boolean;
|
||||
onEdit: () => void;
|
||||
onSave: (next: DashboardtypesThresholdWithLabelDTO) => void;
|
||||
onLiveChange: (next: DashboardtypesThresholdWithLabelDTO) => void;
|
||||
onDiscard: () => void;
|
||||
onRemove: () => void;
|
||||
}
|
||||
@@ -33,15 +32,10 @@ function LabelThresholdRow({
|
||||
isEditing,
|
||||
onEdit,
|
||||
onSave,
|
||||
onLiveChange,
|
||||
onDiscard,
|
||||
onRemove,
|
||||
}: LabelThresholdRowProps): JSX.Element {
|
||||
const { draft, setDraft, setValue } = useThresholdDraft(
|
||||
threshold,
|
||||
isEditing,
|
||||
onLiveChange,
|
||||
);
|
||||
const { draft, setDraft, setValue } = useThresholdDraft(threshold, isEditing);
|
||||
|
||||
// Persist an empty-string label when none was entered — the spec requires a string.
|
||||
const handleSave = useCallback((): void => {
|
||||
|
||||
@@ -28,7 +28,6 @@ interface TableThresholdRowProps {
|
||||
isEditing: boolean;
|
||||
onEdit: () => void;
|
||||
onSave: (next: DashboardtypesTableThresholdDTO) => void;
|
||||
onLiveChange: (next: DashboardtypesTableThresholdDTO) => void;
|
||||
onDiscard: () => void;
|
||||
onRemove: () => void;
|
||||
}
|
||||
@@ -46,15 +45,10 @@ function TableThresholdRow({
|
||||
isEditing,
|
||||
onEdit,
|
||||
onSave,
|
||||
onLiveChange,
|
||||
onDiscard,
|
||||
onRemove,
|
||||
}: TableThresholdRowProps): JSX.Element {
|
||||
const { draft, setDraft, setValue } = useThresholdDraft(
|
||||
threshold,
|
||||
isEditing,
|
||||
onLiveChange,
|
||||
);
|
||||
const { draft, setDraft, setValue } = useThresholdDraft(threshold, isEditing);
|
||||
|
||||
// Stored columnName is the query key; resolve its label + configured unit.
|
||||
const columnUnit = tableColumns.find((c) => c.key === draft.columnName)?.unit;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
|
||||
interface ThresholdDraft<T> {
|
||||
draft: T;
|
||||
@@ -8,25 +7,17 @@ interface ThresholdDraft<T> {
|
||||
setValue: (raw: string) => void;
|
||||
}
|
||||
|
||||
const LIVE_PREVIEW_DEBOUNCE_MS = 150;
|
||||
|
||||
/**
|
||||
* Local draft for a threshold row, shared by every variant. Snapshots the saved
|
||||
* threshold on each entry into edit mode and exposes the numeric `value` setter all
|
||||
* variants use. `onLiveChange` mirrors the draft into the spec as the user edits, so the
|
||||
* panel preview updates live (without Save); the section reverts it on Discard.
|
||||
* threshold on each entry into edit mode (so Discard simply drops the draft and the
|
||||
* next edit starts clean) and exposes the numeric `value` setter all variants use.
|
||||
*/
|
||||
export function useThresholdDraft<T extends { value: number }>(
|
||||
threshold: T,
|
||||
isEditing: boolean,
|
||||
onLiveChange?: (draft: T) => void,
|
||||
): ThresholdDraft<T> {
|
||||
const [draft, setDraft] = useState<T>(threshold);
|
||||
|
||||
const emitLiveChange = useDebouncedFn((next) => {
|
||||
onLiveChange?.(next as T);
|
||||
}, LIVE_PREVIEW_DEBOUNCE_MS);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
setDraft(threshold);
|
||||
@@ -34,20 +25,6 @@ export function useThresholdDraft<T extends { value: number }>(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- snapshot only on edit entry
|
||||
}, [isEditing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
emitLiveChange(draft);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- propagate on draft change only
|
||||
}, [draft]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditing) {
|
||||
emitLiveChange.cancel();
|
||||
}
|
||||
return (): void => emitLiveChange.cancel();
|
||||
}, [isEditing, emitLiveChange]);
|
||||
|
||||
const setValue = (raw: string): void => {
|
||||
const next = Number(raw);
|
||||
setDraft((d) => ({ ...d, value: Number.isNaN(next) ? d.value : next }));
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
DashboardtypesComparisonOperatorDTO,
|
||||
type DashboardtypesPanelSpecDTO,
|
||||
DashboardtypesThresholdFormatDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
type DashboardtypesPanelSpecDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { getPanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/registry';
|
||||
|
||||
@@ -97,141 +95,4 @@ describe('getSwitchedPluginSpec', () => {
|
||||
|
||||
expect(result.legend?.position).toBe('bottom');
|
||||
});
|
||||
|
||||
describe('thresholds', () => {
|
||||
it('does not carry thresholds when the new kind has no thresholds section', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({ sections: [{ kind: 'columns' }] });
|
||||
const old = specWith({
|
||||
thresholds: [{ value: 80, color: '#F1575F', label: 'warn' }],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/ListPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.thresholds).toBeUndefined();
|
||||
});
|
||||
|
||||
it('carries thresholds verbatim within the label variant (color/value/unit/label)', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'thresholds', controls: { variant: 'label' } }],
|
||||
});
|
||||
const old = specWith({
|
||||
thresholds: [{ value: 80, color: '#F1575F', unit: 'ms', label: 'warn' }],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/BarChartPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.thresholds).toStrictEqual([
|
||||
{ value: 80, color: '#F1575F', unit: 'ms', label: 'warn' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('remaps label thresholds into the comparison variant, defaulting operator + format', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'thresholds', controls: { variant: 'comparison' } }],
|
||||
});
|
||||
const old = specWith({
|
||||
thresholds: [{ value: 80, color: '#F1575F', label: 'warn' }],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/NumberPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
// The label is dropped; operator/format are seeded so the threshold can match.
|
||||
expect(result.thresholds).toStrictEqual([
|
||||
{
|
||||
value: 80,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.above,
|
||||
format: DashboardtypesThresholdFormatDTO.text,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('remaps comparison thresholds into the table variant, keeping operator/format and seeding a column', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'thresholds', controls: { variant: 'table' } }],
|
||||
});
|
||||
const old = specWith({
|
||||
thresholds: [
|
||||
{
|
||||
value: 80,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.below,
|
||||
format: DashboardtypesThresholdFormatDTO.text,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/TablePanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.thresholds).toStrictEqual([
|
||||
{
|
||||
value: 80,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.below,
|
||||
format: DashboardtypesThresholdFormatDTO.text,
|
||||
columnName: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('drops the table-only columnName when remapping into the label variant', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'thresholds', controls: { variant: 'label' } }],
|
||||
});
|
||||
const old = specWith({
|
||||
thresholds: [
|
||||
{
|
||||
value: 80,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.above,
|
||||
format: DashboardtypesThresholdFormatDTO.background,
|
||||
columnName: 'p99',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/TimeSeriesPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.thresholds).toStrictEqual([{ value: 80, color: '#F1575F' }]);
|
||||
});
|
||||
|
||||
it('defaults the variant to label when the thresholds section omits controls', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'thresholds', controls: {} }],
|
||||
});
|
||||
const old = specWith({
|
||||
thresholds: [{ value: 80, color: '#F1575F', label: 'warn' }],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/TimeSeriesPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.thresholds).toStrictEqual([
|
||||
{ value: 80, color: '#F1575F', label: 'warn' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import {
|
||||
DashboardtypesComparisonOperatorDTO,
|
||||
type DashboardtypesPanelSpecDTO,
|
||||
DashboardtypesThresholdFormatDTO,
|
||||
type TelemetrytypesSignalDTO,
|
||||
type TelemetrytypesTelemetryFieldKeyDTO,
|
||||
import type {
|
||||
DashboardtypesPanelSpecDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
TelemetrytypesTelemetryFieldKeyDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { getPanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/registry';
|
||||
import type { PanelKind } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/panelKind';
|
||||
import {
|
||||
type AnyThreshold,
|
||||
type PanelFormattingSlice,
|
||||
type SectionConfig,
|
||||
SectionKind,
|
||||
type ThresholdVariant,
|
||||
type PanelFormattingSlice,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
import {
|
||||
buildDefaultPluginSpec,
|
||||
@@ -29,73 +24,13 @@ import { defaultColumnsForSignal } from './ListColumnsEditor/selectFields';
|
||||
export interface SwitchedPluginSpec extends DefaultPluginSpec {
|
||||
formatting?: Pick<PanelFormattingSlice, 'unit' | 'decimalPrecision'>;
|
||||
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
thresholds?: AnyThreshold[];
|
||||
}
|
||||
|
||||
/** Every field any threshold variant can hold; switching reads across shapes to remap. */
|
||||
interface AnyThresholdFields {
|
||||
color: string;
|
||||
value: number;
|
||||
unit?: string;
|
||||
operator?: DashboardtypesComparisonOperatorDTO;
|
||||
format?: DashboardtypesThresholdFormatDTO;
|
||||
columnName?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/** The threshold variant a kind edits, or `undefined` when it has no Thresholds section. */
|
||||
function getThresholdVariant(
|
||||
sections: SectionConfig[],
|
||||
): ThresholdVariant | undefined {
|
||||
const section = sections.find(
|
||||
(s): s is Extract<SectionConfig, { kind: SectionKind.Thresholds }> =>
|
||||
s.kind === SectionKind.Thresholds,
|
||||
);
|
||||
return section ? (section.controls.variant ?? 'label') : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remaps a threshold to the target kind's variant: keeps the shared core (color, value,
|
||||
* unit) plus any cross-variant fields, and seeds the rest with the variant's defaults so
|
||||
* the carried threshold stays functional (a comparison/table threshold needs an operator
|
||||
* to match, a table threshold a column).
|
||||
*/
|
||||
function toThresholdVariant(
|
||||
source: AnyThresholdFields,
|
||||
variant: ThresholdVariant,
|
||||
): AnyThreshold {
|
||||
const core = {
|
||||
color: source.color,
|
||||
value: source.value,
|
||||
...(source.unit !== undefined && { unit: source.unit }),
|
||||
};
|
||||
if (variant === 'comparison') {
|
||||
return {
|
||||
...core,
|
||||
operator: source.operator ?? DashboardtypesComparisonOperatorDTO.above,
|
||||
format: source.format ?? DashboardtypesThresholdFormatDTO.text,
|
||||
};
|
||||
}
|
||||
if (variant === 'table') {
|
||||
return {
|
||||
...core,
|
||||
operator: source.operator ?? DashboardtypesComparisonOperatorDTO.above,
|
||||
format: source.format ?? DashboardtypesThresholdFormatDTO.background,
|
||||
columnName: source.columnName ?? '',
|
||||
};
|
||||
}
|
||||
return {
|
||||
...core,
|
||||
...(source.label !== undefined && { label: source.label }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the plugin spec for a first-visit switch to `newKind`: the kind's declared
|
||||
* section defaults (so the config pane opens populated, matching new-panel seeding) plus
|
||||
* the cross-kind config worth keeping — unit + decimal precision, and thresholds when the
|
||||
* new kind supports them (remapped to its variant). Switching into a List seeds the
|
||||
* current signal's default columns so the columns control isn't empty.
|
||||
* the only cross-kind config worth keeping — unit + decimal precision. Switching into a
|
||||
* List seeds the current signal's default columns so the columns control isn't empty.
|
||||
*
|
||||
* Revisiting a kind restores its stashed spec instead, so this runs only on first visit.
|
||||
*/
|
||||
@@ -131,19 +66,5 @@ export function getSwitchedPluginSpec(
|
||||
}
|
||||
}
|
||||
|
||||
const thresholdVariant = getThresholdVariant(sections);
|
||||
if (thresholdVariant) {
|
||||
const oldThresholds = (
|
||||
oldSpec.plugin.spec as {
|
||||
thresholds?: AnyThreshold[] | null;
|
||||
}
|
||||
).thresholds;
|
||||
if (oldThresholds && oldThresholds.length > 0) {
|
||||
result.thresholds = oldThresholds.map((threshold) =>
|
||||
toThresholdVariant(threshold as AnyThresholdFields, thresholdVariant),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import useGetYAxisUnit from 'hooks/useGetYAxisUnit';
|
||||
|
||||
import { useMetricYAxisUnit } from '../useMetricYAxisUnit';
|
||||
|
||||
jest.mock('hooks/useGetYAxisUnit', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseGetYAxisUnit = useGetYAxisUnit as unknown as jest.Mock;
|
||||
|
||||
function mockMetricUnit(
|
||||
yAxisUnit: string | undefined,
|
||||
isLoading = false,
|
||||
): void {
|
||||
mockUseGetYAxisUnit.mockReturnValue({ yAxisUnit, isLoading, isError: false });
|
||||
}
|
||||
|
||||
describe('useMetricYAxisUnit', () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('seeds the unit from the metric on a new panel', () => {
|
||||
mockMetricUnit('bytes');
|
||||
const onSelectUnit = jest.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useMetricYAxisUnit({ isNewPanel: true, unit: undefined, onSelectUnit }),
|
||||
);
|
||||
|
||||
expect(onSelectUnit).toHaveBeenCalledWith('bytes');
|
||||
});
|
||||
|
||||
it('does not seed when not a new panel', () => {
|
||||
mockMetricUnit('bytes');
|
||||
const onSelectUnit = jest.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useMetricYAxisUnit({ isNewPanel: false, unit: undefined, onSelectUnit }),
|
||||
);
|
||||
|
||||
expect(onSelectUnit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not seed when the metric has no unit', () => {
|
||||
mockMetricUnit(undefined);
|
||||
const onSelectUnit = jest.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useMetricYAxisUnit({ isNewPanel: true, unit: undefined, onSelectUnit }),
|
||||
);
|
||||
|
||||
expect(onSelectUnit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not seed when the unit already matches the metric', () => {
|
||||
mockMetricUnit('bytes');
|
||||
const onSelectUnit = jest.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useMetricYAxisUnit({ isNewPanel: true, unit: 'bytes', onSelectUnit }),
|
||||
);
|
||||
|
||||
expect(onSelectUnit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('re-seeds when the resolved metric unit changes', () => {
|
||||
mockMetricUnit('bytes');
|
||||
const onSelectUnit = jest.fn();
|
||||
|
||||
const { rerender } = renderHook(
|
||||
(props: { unit: string | undefined }) =>
|
||||
useMetricYAxisUnit({
|
||||
isNewPanel: true,
|
||||
unit: props.unit,
|
||||
onSelectUnit,
|
||||
}),
|
||||
{ initialProps: { unit: undefined as string | undefined } },
|
||||
);
|
||||
expect(onSelectUnit).toHaveBeenLastCalledWith('bytes');
|
||||
|
||||
// The metric changes; the panel now holds the previously-seeded unit.
|
||||
mockMetricUnit('ms');
|
||||
rerender({ unit: 'bytes' });
|
||||
|
||||
expect(onSelectUnit).toHaveBeenLastCalledWith('ms');
|
||||
});
|
||||
|
||||
it('returns the resolved metric unit and loading state', () => {
|
||||
mockMetricUnit('bytes', true);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useMetricYAxisUnit({
|
||||
isNewPanel: false,
|
||||
unit: undefined,
|
||||
onSelectUnit: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.metricUnit).toBe('bytes');
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,36 +1,40 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import {
|
||||
getGetDashboardV2QueryKey,
|
||||
usePatchDashboardV2,
|
||||
} from 'api/generated/services/dashboard';
|
||||
import type { DashboardtypesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { usePanelEditorSave } from '../usePanelEditorSave';
|
||||
|
||||
const mockPatchAsync = jest.fn().mockResolvedValue(undefined);
|
||||
let mockIsPatching = false;
|
||||
jest.mock('../../../hooks/useOptimisticPatch', () => ({
|
||||
useOptimisticPatch: (): {
|
||||
patchAsync: jest.Mock;
|
||||
isPatching: boolean;
|
||||
error: Error | null;
|
||||
} => ({ patchAsync: mockPatchAsync, isPatching: mockIsPatching, error: null }),
|
||||
}));
|
||||
|
||||
// The hook reads getQueryData only for the isNew branch; a stub client is enough here.
|
||||
const mockInvalidateQueries = jest.fn();
|
||||
jest.mock('react-query', () => ({
|
||||
useQueryClient: (): { getQueryData: jest.Mock } => ({
|
||||
getQueryData: jest.fn(),
|
||||
useQueryClient: (): { invalidateQueries: jest.Mock } => ({
|
||||
invalidateQueries: mockInvalidateQueries,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('api/generated/services/dashboard', () => ({
|
||||
usePatchDashboardV2: jest.fn(),
|
||||
getGetDashboardV2QueryKey: jest.fn(() => ['/api/v2/dashboards/dash-1']),
|
||||
}));
|
||||
|
||||
const mockUsePatch = usePatchDashboardV2 as unknown as jest.Mock;
|
||||
const mockGetQueryKey = getGetDashboardV2QueryKey as unknown as jest.Mock;
|
||||
|
||||
describe('usePanelEditorSave', () => {
|
||||
const mutateAsync = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockIsPatching = false;
|
||||
mockUsePatch.mockReturnValue({
|
||||
mutateAsync,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('optimistically patches an add replacing the whole panel spec', async () => {
|
||||
it('emits an add patch replacing the whole panel spec and invalidates the dashboard query', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelEditorSave({ dashboardId: 'dash-1', panelId: 'panel-9' }),
|
||||
);
|
||||
@@ -46,17 +50,28 @@ describe('usePanelEditorSave', () => {
|
||||
|
||||
await result.current.save(spec);
|
||||
|
||||
expect(mockPatchAsync).toHaveBeenCalledWith([
|
||||
{
|
||||
op: 'add',
|
||||
path: '/spec/panels/panel-9/spec',
|
||||
value: spec,
|
||||
},
|
||||
expect(mutateAsync).toHaveBeenCalledWith({
|
||||
pathParams: { id: 'dash-1' },
|
||||
data: [
|
||||
{
|
||||
op: 'add',
|
||||
path: '/spec/panels/panel-9/spec',
|
||||
value: spec,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(mockGetQueryKey).toHaveBeenCalledWith({ id: 'dash-1' });
|
||||
expect(mockInvalidateQueries).toHaveBeenCalledWith([
|
||||
'/api/v2/dashboards/dash-1',
|
||||
]);
|
||||
});
|
||||
|
||||
it('surfaces the patch in-flight state as isSaving', () => {
|
||||
mockIsPatching = true;
|
||||
it('surfaces the mutation loading state as isSaving', () => {
|
||||
mockUsePatch.mockReturnValue({
|
||||
mutateAsync,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelEditorSave({ dashboardId: 'dash-1', panelId: 'panel-9' }),
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import useGetYAxisUnit from 'hooks/useGetYAxisUnit';
|
||||
|
||||
interface UseMetricYAxisUnitArgs {
|
||||
/** Only a new panel auto-seeds; editing never overwrites the saved unit. */
|
||||
isNewPanel: boolean;
|
||||
unit: string | undefined;
|
||||
onSelectUnit: (unit: string) => void;
|
||||
}
|
||||
|
||||
interface UseMetricYAxisUnitResult {
|
||||
metricUnit: string | undefined;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the selected metric's unit and, on a new panel only, seeds the formatting unit
|
||||
* from it (V1 parity); returns the unit for the selector's mismatch warning.
|
||||
*/
|
||||
export function useMetricYAxisUnit({
|
||||
isNewPanel,
|
||||
unit,
|
||||
onSelectUnit,
|
||||
}: UseMetricYAxisUnitArgs): UseMetricYAxisUnitResult {
|
||||
const { yAxisUnit: metricUnit, isLoading } = useGetYAxisUnit();
|
||||
|
||||
useEffect(() => {
|
||||
if (isNewPanel && metricUnit && metricUnit !== unit) {
|
||||
onSelectUnit(metricUnit);
|
||||
}
|
||||
// Re-seed only when the resolved metric unit changes, not on every unit edit.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isNewPanel, metricUnit]);
|
||||
|
||||
return { metricUnit, isLoading };
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user