mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-19 00:20:40 +01:00
Compare commits
1 Commits
main
...
refactor/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5afc3c1439 |
@@ -1,5 +1,7 @@
|
||||
import { DrawerProps } from 'antd';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
||||
import { ActionItemProps } from 'container/LogDetailedView/LogDetailedView.types';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
@@ -18,7 +20,9 @@ export type LogDetailProps = {
|
||||
handleOpenInExplorer?: MouseEventHandler;
|
||||
getContainer?: DrawerProps['getContainer'];
|
||||
onApplyLogFilter?: (expression: string) => void;
|
||||
} & Pick<DrawerProps, 'onClose'>;
|
||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
|
||||
Pick<DrawerProps, 'onClose'>;
|
||||
|
||||
export type LogDetailInnerProps = LogDetailProps & {
|
||||
log: NonNullable<LogDetailProps['log']>;
|
||||
|
||||
@@ -46,6 +46,8 @@ function renderDrawer(props: Partial<LogDetailProps> = {}): void {
|
||||
<LogDetail
|
||||
log={mockLog}
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
onAddToQuery={jest.fn()}
|
||||
onClickActionItem={jest.fn()}
|
||||
onClose={jest.fn()}
|
||||
{...props}
|
||||
/>,
|
||||
|
||||
@@ -25,8 +25,10 @@ import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants'
|
||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog';
|
||||
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
|
||||
import useScrollToLog from 'hooks/logs/useScrollToLog';
|
||||
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
||||
import { saveRecentQueryByExpression } from 'lib/recentQueries/saveRecentQuery';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -80,6 +82,29 @@ function EntityLogsContent({
|
||||
const { activeLog, selectedTab, handleSetActiveLog, handleCloseLogDetail } =
|
||||
useLogDetailHandlers();
|
||||
|
||||
// TODO: Move away from using onAddToQuery after old drawer cleanup
|
||||
const onAddToQuery = useCallback(
|
||||
(fieldKey: string, fieldValue: string, operator: string): void => {
|
||||
handleCloseLogDetail();
|
||||
|
||||
const partExpression = generateFilterQuery({
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
type: getOldLogsOperatorFromNew(operator),
|
||||
});
|
||||
|
||||
const currentUser = userExpression;
|
||||
const newUser = currentUser.trim()
|
||||
? `${currentUser} AND ${partExpression}`
|
||||
: partExpression;
|
||||
|
||||
querySearchOnRun(newUser);
|
||||
|
||||
logInfraDrawerFilterCustomizedEvent(category, 'logs', newUser, 'logs');
|
||||
},
|
||||
[userExpression, querySearchOnRun, handleCloseLogDetail, category],
|
||||
);
|
||||
|
||||
const onApplyLogFilter = useCallback(
|
||||
(expression: string): void => {
|
||||
handleCloseLogDetail();
|
||||
@@ -317,6 +342,8 @@ function EntityLogsContent({
|
||||
logs={logs}
|
||||
onNavigateLog={handleSetActiveLog}
|
||||
selectedTab={selectedTab}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
onApplyLogFilter={onApplyLogFilter}
|
||||
onScrollToLog={handleScrollToLog}
|
||||
handleOpenInExplorer={(e) => handleOpenInExplorer(e, activeLog)}
|
||||
|
||||
@@ -259,6 +259,8 @@ function LiveLogsList({
|
||||
selectedTab={selectedTab}
|
||||
log={activeLog}
|
||||
onClose={handleCloseLogDetail}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
logs={formattedLogs}
|
||||
onNavigateLog={handleSetActiveLog}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export type AnyObject = { [key: string]: any };
|
||||
|
||||
@@ -18,3 +19,15 @@ export interface DataType {
|
||||
field: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ActionItemProps {
|
||||
fieldKey: string;
|
||||
fieldValue: string;
|
||||
onClickActionItem: (
|
||||
fieldKey: string,
|
||||
fieldValue: string,
|
||||
operator: string,
|
||||
dataType?: DataTypes,
|
||||
fieldType?: string,
|
||||
) => void;
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ const normalizeDataType = (
|
||||
// key is fabricated locally (empty source list), so no click-time getAggregateKeys fetch.
|
||||
export const getFilterQueryData = (
|
||||
item: IBuilderQuery,
|
||||
target: Pick<LogFilterTarget, 'fieldKey' | 'dataType' | 'metricsType'>,
|
||||
target: LogFilterTarget,
|
||||
value: unknown,
|
||||
operator: string,
|
||||
): IBuilderQuery => {
|
||||
|
||||
@@ -381,6 +381,8 @@ function LogsExplorerList({
|
||||
selectedTab={selectedTab}
|
||||
log={activeLog}
|
||||
onClose={handleCloseLogDetail}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
logs={logs}
|
||||
onNavigateLog={handleSetActiveLog}
|
||||
|
||||
@@ -91,8 +91,13 @@ function LogsPanelComponent({
|
||||
() => logs.map((log) => FlatLogData(log) as RowData),
|
||||
[logs],
|
||||
);
|
||||
const { activeLog, selectedTab, handleSetActiveLog, handleCloseLogDetail } =
|
||||
useLogDetailHandlers();
|
||||
const {
|
||||
activeLog,
|
||||
onAddToQuery,
|
||||
selectedTab,
|
||||
handleSetActiveLog,
|
||||
handleCloseLogDetail,
|
||||
} = useLogDetailHandlers();
|
||||
|
||||
const handleRow = useCallback(
|
||||
(record: RowData): HTMLAttributes<RowData> => ({
|
||||
@@ -174,6 +179,8 @@ function LogsPanelComponent({
|
||||
selectedTab={selectedTab}
|
||||
log={activeLog}
|
||||
onClose={handleCloseLogDetail}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
isListViewPanel
|
||||
listViewPanelSelectedFields={widget?.selectedLogFields}
|
||||
logs={logs}
|
||||
|
||||
@@ -10,7 +10,8 @@ import { ILog } from 'types/api/logs/log';
|
||||
import './styles.scss';
|
||||
|
||||
function LogsList({ logs }: LogsListProps): JSX.Element {
|
||||
const { activeLog, onSetActiveLog, onClearActiveLog } = useActiveLog();
|
||||
const { activeLog, onSetActiveLog, onClearActiveLog, onAddToQuery } =
|
||||
useActiveLog();
|
||||
|
||||
const makeLogDetailsHandler = (log: ILog) => (): void => onSetActiveLog(log);
|
||||
|
||||
@@ -44,6 +45,8 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
gap: var(--spacing-4);
|
||||
gap: var(--spacing-1);
|
||||
}
|
||||
|
||||
.collapseIcon {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import { rest, type RequestHandler } from 'msw';
|
||||
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
import remove from 'api/browser/localstorage/remove';
|
||||
import {
|
||||
type AuthtypesTransactionDTO,
|
||||
@@ -12,13 +11,11 @@ import {
|
||||
SavedviewtypesSourceDTO,
|
||||
type SavedviewtypesUpdatableSavedViewDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { SIDEBAR_TOOLTIP_DELAY_MS } from 'components/QuickFilters/QuickFiltersLayout/constants';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import {
|
||||
AUTHZ_CHECK_URL,
|
||||
authzMockResponse,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
import { expect, screen, userEvent, waitFor, within } from 'storybook/test';
|
||||
|
||||
import { accessFor } from '@/storybook/access/access';
|
||||
import {
|
||||
@@ -28,7 +25,6 @@ import {
|
||||
} from '@/storybook/controls/controls';
|
||||
import { defineStoryMocks } from '@/storybook/controls/defineStoryMocks';
|
||||
import type { MockControlValues, StoryMocks } from '@/storybook/controls/types';
|
||||
import { withCanvas } from '@/storybook/decorators/withCanvas';
|
||||
import { authzMocks, type AuthzArgs } from '@/storybook/globals/authzMocks';
|
||||
import { respondWith } from '@/storybook/runtime/responseState';
|
||||
|
||||
@@ -47,10 +43,6 @@ import {
|
||||
unreadableViewId,
|
||||
} from './__story_mockdata__/savedViews';
|
||||
|
||||
import SavedViewsHeader from '../SavedViewsHeader';
|
||||
import SavedViewsPanel from '../SavedViewsPanel';
|
||||
import type { SavedViewRowActionKey } from '../types';
|
||||
|
||||
const API = 'http://localhost/api/v2/saved_views';
|
||||
|
||||
const LIST = 'Saved Views · list';
|
||||
@@ -224,106 +216,3 @@ export const savedViewsMocks: StoryMocks<typeof controls> = {
|
||||
...definition,
|
||||
read: (args) => ({ ...authzMocks.read(args), ...definition.read(args) }),
|
||||
};
|
||||
|
||||
/** The sidebar slot of an explorer without quick filters. */
|
||||
export const sidebarCanvas = withCanvas({
|
||||
width: 280,
|
||||
height: 640,
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRight: '1px solid var(--l1-border)',
|
||||
});
|
||||
|
||||
/**
|
||||
* The header above the pinned list. `onOpenViews` adds All views, which the
|
||||
* header only shows in an explorer whose quick filters hide the list.
|
||||
*/
|
||||
export function SavedViewsSidebar({
|
||||
source,
|
||||
onOpenViews,
|
||||
}: {
|
||||
source: SavedViewSource;
|
||||
onOpenViews?: () => void;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<TooltipProvider delay={SIDEBAR_TOOLTIP_DELAY_MS}>
|
||||
<SavedViewsHeader source={source} onOpenViews={onOpenViews} />
|
||||
<SavedViewsPanel source={source} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/** The panel fetches the list and checks every row's permissions before it settles. */
|
||||
export const UNTIL_LOADED = { timeout: 15_000 };
|
||||
|
||||
/**
|
||||
* Re-queried on every attempt: a button whose check settles is a new node, so a
|
||||
* reference taken while it was pending never changes.
|
||||
*/
|
||||
type FindElement = () => HTMLElement;
|
||||
|
||||
export const byTestId =
|
||||
(testId: string): FindElement =>
|
||||
(): HTMLElement =>
|
||||
screen.getByTestId(testId);
|
||||
|
||||
/** A check still in flight: `aria-busy` on a button, `data-loading` on a menu row. */
|
||||
const isPending = (element: HTMLElement): boolean =>
|
||||
element.getAttribute('aria-busy') === 'true' ||
|
||||
element.hasAttribute('data-loading');
|
||||
|
||||
const isDisabled = (element: HTMLElement): boolean =>
|
||||
element.hasAttribute('disabled') || element.hasAttribute('data-disabled');
|
||||
|
||||
export const expectDisabled = (
|
||||
find: FindElement,
|
||||
disabled = true,
|
||||
): Promise<void> =>
|
||||
waitFor(async () => {
|
||||
const element = find();
|
||||
|
||||
await expect(isPending(element)).toBe(false);
|
||||
await expect(isDisabled(element)).toBe(disabled);
|
||||
}, UNTIL_LOADED);
|
||||
|
||||
export const expectPending = (find: FindElement): Promise<void> =>
|
||||
waitFor(() => expect(isPending(find())).toBe(true), UNTIL_LOADED);
|
||||
|
||||
export const findRows = (canvasElement: HTMLElement): Promise<HTMLElement[]> =>
|
||||
within(canvasElement).findAllByTestId(
|
||||
'saved-views-row',
|
||||
undefined,
|
||||
UNTIL_LOADED,
|
||||
);
|
||||
|
||||
export const rowName =
|
||||
(row: HTMLElement): FindElement =>
|
||||
(): HTMLElement =>
|
||||
within(row).getByTestId('saved-views-row-name');
|
||||
|
||||
export const openRowMenu = async (row: HTMLElement): Promise<void> => {
|
||||
await userEvent.click(within(row).getByTestId('saved-views-row-menu'));
|
||||
await screen.findByRole('menu', undefined, UNTIL_LOADED);
|
||||
};
|
||||
|
||||
export const rowMenuItem = (key: SavedViewRowActionKey): FindElement =>
|
||||
byTestId(`saved-views-row-menu-item-${key}`);
|
||||
|
||||
export const openSaveChangesMenu = async (
|
||||
canvasElement: HTMLElement,
|
||||
): Promise<void> => {
|
||||
const trigger = await waitFor(async () => {
|
||||
const button = within(canvasElement).getByTestId('saved-views-save-changes');
|
||||
|
||||
await expect(button).not.toHaveAttribute('aria-busy');
|
||||
|
||||
return button;
|
||||
}, UNTIL_LOADED);
|
||||
|
||||
await userEvent.click(trigger);
|
||||
await screen.findByRole('menu', undefined, UNTIL_LOADED);
|
||||
};
|
||||
|
||||
export const saveChangesItem = (key: 'save-as-new' | 'update'): FindElement =>
|
||||
byTestId(`saved-views-save-changes-item-${key}`);
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
import { SIDEBAR_TOOLTIP_DELAY_MS } from 'components/QuickFilters/QuickFiltersLayout/constants';
|
||||
import { expect, screen, userEvent, waitFor, within } from 'storybook/test';
|
||||
|
||||
import { storyMocks } from '@/storybook/controls/defineStoryMocks';
|
||||
import { withCanvas } from '@/storybook/decorators/withCanvas';
|
||||
import type { PageStoryArgs } from '@/storybook/runtime/resolveStory';
|
||||
|
||||
import SavedViewsHeader from '../SavedViewsHeader';
|
||||
import SavedViewsPanel from '../SavedViewsPanel';
|
||||
import {
|
||||
findRows,
|
||||
openRowMenu,
|
||||
openSaveChangesMenu,
|
||||
SavedViewsSidebar,
|
||||
savedViewsMocks,
|
||||
sidebarCanvas,
|
||||
} from './SavedViews.stories.mocks';
|
||||
import { savedViewsMocks } from './SavedViews.stories.mocks';
|
||||
|
||||
type SavedViewsArgs = PageStoryArgs<typeof savedViewsMocks>;
|
||||
|
||||
const componentStory = storyMocks(savedViewsMocks);
|
||||
|
||||
/** The panel fetches the list and checks every row's permissions before it settles. */
|
||||
const untilLoaded = { timeout: 15_000 };
|
||||
|
||||
/**
|
||||
* The saved views sidebar of an explorer without quick filters: the header with
|
||||
* the open view and its actions, above the list of views created by the user
|
||||
@@ -27,11 +28,25 @@ const componentStory = storyMocks(savedViewsMocks);
|
||||
* `viewKey`.
|
||||
*/
|
||||
const meta = {
|
||||
title: 'Components/Saved Views/Overview',
|
||||
title: 'Components/Saved Views',
|
||||
tags: ['authz', 'play'],
|
||||
component: SavedViewsPanel,
|
||||
decorators: [sidebarCanvas],
|
||||
render: ({ source }): JSX.Element => <SavedViewsSidebar source={source} />,
|
||||
decorators: [
|
||||
withCanvas({
|
||||
width: 280,
|
||||
height: 640,
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRight: '1px solid var(--l1-border)',
|
||||
}),
|
||||
],
|
||||
render: ({ source }): JSX.Element => (
|
||||
<TooltipProvider delay={SIDEBAR_TOOLTIP_DELAY_MS}>
|
||||
<SavedViewsHeader source={source} />
|
||||
<SavedViewsPanel source={source} />
|
||||
</TooltipProvider>
|
||||
),
|
||||
...componentStory,
|
||||
parameters: { ...componentStory.parameters },
|
||||
} satisfies Meta<SavedViewsArgs>;
|
||||
@@ -56,15 +71,25 @@ export const UnsavedChanges: Story = {
|
||||
args: { openView: 'unsaved-changes' },
|
||||
};
|
||||
|
||||
/** Without `list` on saved views, the panel shows the permission callout. */
|
||||
export const NoAccess: Story = {
|
||||
args: { revoked: ['list:saved-view'] },
|
||||
};
|
||||
|
||||
/**
|
||||
* The first row's menu. Edit details and Delete follow `update` and `delete` on
|
||||
* that view, so revoking either disables its item.
|
||||
*/
|
||||
export const RowMenu: Story = {
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
const [row] = await findRows(canvasElement);
|
||||
const [row] = await within(canvasElement).findAllByTestId(
|
||||
'saved-views-row',
|
||||
undefined,
|
||||
untilLoaded,
|
||||
);
|
||||
|
||||
await openRowMenu(row);
|
||||
await userEvent.click(within(row).getByTestId('saved-views-row-menu'));
|
||||
await screen.findByRole('menu', undefined, untilLoaded);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -75,6 +100,15 @@ export const RowMenu: Story = {
|
||||
export const SaveChangesMenu: Story = {
|
||||
args: { openView: 'unsaved-changes' },
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
await openSaveChangesMenu(canvasElement);
|
||||
const trigger = await waitFor(async () => {
|
||||
const button = within(canvasElement).getByTestId('saved-views-save-changes');
|
||||
|
||||
await expect(button).not.toHaveAttribute('aria-busy');
|
||||
|
||||
return button;
|
||||
}, untilLoaded);
|
||||
|
||||
await userEvent.click(trigger);
|
||||
await screen.findByRole('menu', undefined, untilLoaded);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { expect, waitFor, within } from 'storybook/test';
|
||||
|
||||
import { storyMocks } from '@/storybook/controls/defineStoryMocks';
|
||||
import type { PageStoryArgs } from '@/storybook/runtime/resolveStory';
|
||||
|
||||
import { unreadableViewId } from '../__story_mockdata__/savedViews';
|
||||
import {
|
||||
byTestId,
|
||||
expectDisabled,
|
||||
expectPending,
|
||||
findRows,
|
||||
openRowMenu,
|
||||
openSaveChangesMenu,
|
||||
rowMenuItem,
|
||||
rowName,
|
||||
saveChangesItem,
|
||||
SavedViewsSidebar,
|
||||
savedViewsMocks,
|
||||
sidebarCanvas,
|
||||
UNTIL_LOADED,
|
||||
} from '../SavedViews.stories.mocks';
|
||||
|
||||
import SavedViewsPanel from '../../SavedViewsPanel';
|
||||
|
||||
type SavedViewsArgs = PageStoryArgs<typeof savedViewsMocks>;
|
||||
|
||||
const componentStory = storyMocks(savedViewsMocks);
|
||||
|
||||
/**
|
||||
* The saved views sidebar with one permission missing at a time. Every story
|
||||
* here is an admin denied exactly what its name says. `list` and `create` are
|
||||
* workspace-wide; `read`, `update` and `delete` are checked per view, so a row
|
||||
* menu only checks its view once it is opened.
|
||||
*
|
||||
* Route: the explorer of the Source control, with the Open view control's
|
||||
* `viewKey`.
|
||||
*/
|
||||
const meta = {
|
||||
title: 'Components/Saved Views/Authz',
|
||||
tags: ['authz', 'play'],
|
||||
component: SavedViewsPanel,
|
||||
decorators: [sidebarCanvas],
|
||||
render: ({ source }): JSX.Element => <SavedViewsSidebar source={source} />,
|
||||
...componentStory,
|
||||
parameters: { ...componentStory.parameters },
|
||||
} satisfies Meta<SavedViewsArgs>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<SavedViewsArgs>;
|
||||
|
||||
const LIST = 'list:saved-view';
|
||||
const CREATE = 'create:saved-view';
|
||||
const UPDATE = 'update:saved-view';
|
||||
const DELETE = 'delete:saved-view';
|
||||
|
||||
/**
|
||||
* The panel shows the permission callout in place of the list, and All views,
|
||||
* in an explorer whose quick filters hide the list, is disabled.
|
||||
*/
|
||||
export const NoList: Story = {
|
||||
args: { revoked: [LIST] },
|
||||
render: ({ source }): JSX.Element => (
|
||||
<SavedViewsSidebar source={source} onOpenViews={(): void => {}} />
|
||||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expectDisabled(byTestId('saved-views-open'));
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(canvas.getByTestId('saved-views-panel')).toHaveTextContent(
|
||||
/is not authorized to perform list/,
|
||||
),
|
||||
UNTIL_LOADED,
|
||||
);
|
||||
await expect(canvas.queryByTestId('saved-views-search')).toBeNull();
|
||||
},
|
||||
};
|
||||
|
||||
/** An empty workspace with both ways to create a view disabled: + and Create view. */
|
||||
export const NoCreate: Story = {
|
||||
args: { revoked: [CREATE], createdByMe: 0, createdByOthers: 0 },
|
||||
play: async () => {
|
||||
await expectDisabled(byTestId('saved-views-create'));
|
||||
await expectDisabled(byTestId('saved-views-list-create'));
|
||||
},
|
||||
};
|
||||
|
||||
/** The ✓ menu on unsaved changes: Save as new view disabled, Update selected view open. */
|
||||
export const NoCreateSaveMenu: Story = {
|
||||
args: { revoked: [CREATE], openView: 'unsaved-changes' },
|
||||
play: async ({ canvasElement }) => {
|
||||
await openSaveChangesMenu(canvasElement);
|
||||
await expectDisabled(saveChangesItem('save-as-new'));
|
||||
await expectDisabled(saveChangesItem('update'), false);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* `read` denied on one view made by someone else: its name is disabled and its
|
||||
* hover card never opens, while every other view still opens.
|
||||
*/
|
||||
export const NoRead: Story = {
|
||||
args: { unreadableView: true },
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const rows = await findRows(canvasElement);
|
||||
const deniedId = unreadableViewId(args.source);
|
||||
|
||||
await Promise.all(
|
||||
rows.map((row) =>
|
||||
expectDisabled(rowName(row), row.getAttribute('data-view-id') === deniedId),
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
/** The unreadable view open from its link: the header names it No view access. */
|
||||
export const NoReadOpenView: Story = {
|
||||
args: { unreadableView: true, openView: 'created-by-others' },
|
||||
play: async ({ canvasElement }) => {
|
||||
await expect(
|
||||
await within(canvasElement).findByText(
|
||||
'No view access',
|
||||
undefined,
|
||||
UNTIL_LOADED,
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
/** A row's menu: Edit details disabled; Copy link, Copy ID and Delete open. */
|
||||
export const NoUpdate: Story = {
|
||||
args: { revoked: [UPDATE] },
|
||||
play: async ({ canvasElement }) => {
|
||||
const [row] = await findRows(canvasElement);
|
||||
|
||||
await openRowMenu(row);
|
||||
await expectDisabled(rowMenuItem('editDetails'));
|
||||
await expectDisabled(rowMenuItem('copyLink'), false);
|
||||
await expectDisabled(rowMenuItem('copyId'), false);
|
||||
await expectDisabled(rowMenuItem('delete'), false);
|
||||
},
|
||||
};
|
||||
|
||||
/** The ✓ menu on unsaved changes: Update selected view disabled, Save as new view open. */
|
||||
export const NoUpdateSaveMenu: Story = {
|
||||
args: { revoked: [UPDATE], openView: 'unsaved-changes' },
|
||||
play: async ({ canvasElement }) => {
|
||||
await openSaveChangesMenu(canvasElement);
|
||||
await expectDisabled(saveChangesItem('update'));
|
||||
await expectDisabled(saveChangesItem('save-as-new'), false);
|
||||
},
|
||||
};
|
||||
|
||||
/** A row's menu: Delete disabled, Edit details still open. */
|
||||
export const NoDelete: Story = {
|
||||
args: { revoked: [DELETE] },
|
||||
play: async ({ canvasElement }) => {
|
||||
const [row] = await findRows(canvasElement);
|
||||
|
||||
await openRowMenu(row);
|
||||
await expectDisabled(rowMenuItem('delete'));
|
||||
await expectDisabled(rowMenuItem('editDetails'), false);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Everything but listing and reading: + is disabled, and a row's menu has only
|
||||
* Copy link and Copy ID left.
|
||||
*/
|
||||
export const ReadOnly: Story = {
|
||||
args: { revoked: [CREATE, UPDATE, DELETE] },
|
||||
play: async ({ canvasElement }) => {
|
||||
await expectDisabled(byTestId('saved-views-create'));
|
||||
|
||||
const [row] = await findRows(canvasElement);
|
||||
|
||||
await expectDisabled(rowName(row), false);
|
||||
await openRowMenu(row);
|
||||
await expectDisabled(rowMenuItem('editDetails'));
|
||||
await expectDisabled(rowMenuItem('delete'));
|
||||
await expectDisabled(rowMenuItem('copyLink'), false);
|
||||
await expectDisabled(rowMenuItem('copyId'), false);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The permission check never answering: + spins, and the list stays blank
|
||||
* behind its `list` check.
|
||||
*/
|
||||
export const CheckLoading: Story = {
|
||||
args: { authzState: 'loading' },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expectPending(byTestId('saved-views-create'));
|
||||
await expect(canvas.queryByTestId('saved-views-search')).toBeNull();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The permission check failing rather than denying: the sidebar fails open,
|
||||
* so the list renders and every action stays enabled for the API to refuse.
|
||||
*/
|
||||
export const CheckFailed: Story = {
|
||||
args: { authzState: 'error' },
|
||||
parameters: { allowConsoleErrors: true },
|
||||
play: async ({ canvasElement }) => {
|
||||
await expectDisabled(byTestId('saved-views-create'), false);
|
||||
|
||||
const [row] = await findRows(canvasElement);
|
||||
|
||||
await expectDisabled(rowName(row), false);
|
||||
await openRowMenu(row);
|
||||
await expectDisabled(rowMenuItem('editDetails'), false);
|
||||
await expectDisabled(rowMenuItem('delete'), false);
|
||||
},
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { useActiveLog } from '../useActiveLog';
|
||||
|
||||
const mockRedirect = jest.fn();
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: (): unknown => ({
|
||||
currentQuery: initialQueriesMap.logs,
|
||||
redirectWithQueryBuilderData: mockRedirect,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useUrlQuery', () => ({
|
||||
__esModule: true,
|
||||
default: (): URLSearchParams => new URLSearchParams(),
|
||||
}));
|
||||
|
||||
describe('useActiveLog', () => {
|
||||
beforeEach(() => {
|
||||
mockRedirect.mockClear();
|
||||
});
|
||||
|
||||
it('adds the chip field as a filter with its data type and redirects', () => {
|
||||
const { result } = renderHook(() => useActiveLog());
|
||||
|
||||
result.current.onAddToQuery(
|
||||
'service.name',
|
||||
'frontend',
|
||||
'=',
|
||||
DataTypes.String,
|
||||
);
|
||||
|
||||
expect(mockRedirect).toHaveBeenCalledTimes(1);
|
||||
const query = mockRedirect.mock.calls[0][0] as Query;
|
||||
const item = query.builder.queryData[0].filters?.items.at(-1);
|
||||
expect(item?.key).toMatchObject({
|
||||
key: 'service.name',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
});
|
||||
expect(item?.op).toBe('=');
|
||||
expect(item?.value).toBe('frontend');
|
||||
});
|
||||
|
||||
it('keeps the existing filter items ahead of the new one', () => {
|
||||
const { result } = renderHook(() => useActiveLog());
|
||||
|
||||
result.current.onAddToQuery('k8s.pod.name', 'pod-1', '=');
|
||||
|
||||
const query = mockRedirect.mock.calls[0][0] as Query;
|
||||
const items = query.builder.queryData[0].filters?.items ?? [];
|
||||
expect(items).toHaveLength(
|
||||
(initialQueriesMap.logs.builder.queryData[0].filters?.items.length ?? 0) + 1,
|
||||
);
|
||||
expect(items.at(-1)?.key?.key).toBe('k8s.pod.name');
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ jest.mock('hooks/useIsTextSelected');
|
||||
const mockOnSetActiveLog = jest.fn();
|
||||
const mockOnClearActiveLog = jest.fn();
|
||||
const mockOnAddToQuery = jest.fn();
|
||||
const mockOnGroupByAttribute = jest.fn();
|
||||
const mockIsTextSelected = jest.fn();
|
||||
|
||||
const mockLog: ILog = {
|
||||
@@ -43,6 +44,7 @@ beforeEach(() => {
|
||||
onSetActiveLog: mockOnSetActiveLog,
|
||||
onClearActiveLog: mockOnClearActiveLog,
|
||||
onAddToQuery: mockOnAddToQuery,
|
||||
onGroupByAttribute: mockOnGroupByAttribute,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,6 +80,7 @@ it('should toggle off when clicking the same active log', () => {
|
||||
onSetActiveLog: mockOnSetActiveLog,
|
||||
onClearActiveLog: mockOnClearActiveLog,
|
||||
onAddToQuery: mockOnAddToQuery,
|
||||
onGroupByAttribute: mockOnGroupByAttribute,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useLogDetailHandlers());
|
||||
|
||||
@@ -25,4 +25,5 @@ export type UseActiveLog = {
|
||||
operator: string,
|
||||
dataType?: DataTypes,
|
||||
) => void;
|
||||
onGroupByAttribute: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -1,16 +1,40 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { getFilterQueryData } from 'container/LogDetailedView/logAttributeActions.utils';
|
||||
import { OPERATORS, QueryBuilderKeys } from 'constants/queryBuilder';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { UseActiveLog } from './types';
|
||||
|
||||
export function getOldLogsOperatorFromNew(operator: string): string {
|
||||
switch (operator) {
|
||||
case OPERATORS['=']:
|
||||
return OPERATORS.IN;
|
||||
case OPERATORS['!=']:
|
||||
return OPERATORS.NIN;
|
||||
default:
|
||||
return operator;
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export const useActiveLog = (): UseActiveLog => {
|
||||
const queryClient = useQueryClient();
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const [activeLog, setActiveLog] = useState<ILog | null>(null);
|
||||
|
||||
@@ -34,32 +58,121 @@ export const useActiveLog = (): UseActiveLog => {
|
||||
|
||||
const onClearActiveLog = useCallback((): void => setActiveLog(null), []);
|
||||
|
||||
const onAddToQuery = useCallback(
|
||||
(
|
||||
const onAddToQueryExplorer = useCallback(
|
||||
async (
|
||||
fieldKey: string,
|
||||
fieldValue: string,
|
||||
operator: string,
|
||||
dataType?: DataTypes,
|
||||
): void => {
|
||||
const nextQuery: Query = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item) =>
|
||||
getFilterQueryData(item, { fieldKey, dataType }, fieldValue, operator),
|
||||
),
|
||||
},
|
||||
};
|
||||
fieldType?: MetricsType | undefined,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const keysAutocompleteResponse = await queryClient.fetchQuery(
|
||||
[QueryBuilderKeys.GET_AGGREGATE_KEYS, fieldKey],
|
||||
async () =>
|
||||
getAggregateKeys({
|
||||
searchText: fieldKey,
|
||||
aggregateOperator:
|
||||
currentQuery.builder.queryData[0].aggregateOperator || '',
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute:
|
||||
currentQuery.builder.queryData[0].aggregateAttribute?.key || '',
|
||||
}),
|
||||
);
|
||||
|
||||
redirectWithQueryBuilderData(nextQuery);
|
||||
const keysAutocomplete: BaseAutocompleteData[] =
|
||||
keysAutocompleteResponse.payload?.attributeKeys || [];
|
||||
|
||||
const existAutocompleteKey = chooseAutocompleteFromCustomValue(
|
||||
keysAutocomplete,
|
||||
fieldKey,
|
||||
dataType,
|
||||
fieldType,
|
||||
);
|
||||
|
||||
const currentOperator = getOperatorValue(operator);
|
||||
|
||||
const nextQuery: Query = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: [
|
||||
...(item.filters?.items || []),
|
||||
{
|
||||
id: uuid(),
|
||||
key: existAutocompleteKey,
|
||||
op: currentOperator,
|
||||
value: fieldValue,
|
||||
},
|
||||
],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
redirectWithQueryBuilderData(nextQuery);
|
||||
} catch {
|
||||
notifications.error({ message: SOMETHING_WENT_WRONG });
|
||||
}
|
||||
},
|
||||
[currentQuery, redirectWithQueryBuilderData],
|
||||
[currentQuery, notifications, queryClient, redirectWithQueryBuilderData],
|
||||
);
|
||||
|
||||
const onGroupByAttribute = useCallback(
|
||||
async (fieldKey: string, dataType?: DataTypes): Promise<void> => {
|
||||
try {
|
||||
const keysAutocompleteResponse = await queryClient.fetchQuery(
|
||||
[QueryBuilderKeys.GET_AGGREGATE_KEYS, fieldKey],
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
async () =>
|
||||
getAggregateKeys({
|
||||
searchText: fieldKey,
|
||||
aggregateOperator:
|
||||
currentQuery.builder.queryData[0].aggregateOperator || '',
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute:
|
||||
currentQuery.builder.queryData[0].aggregateAttribute?.key || '',
|
||||
}),
|
||||
);
|
||||
|
||||
const keysAutocomplete: BaseAutocompleteData[] =
|
||||
keysAutocompleteResponse.payload?.attributeKeys || [];
|
||||
|
||||
const existAutocompleteKey = chooseAutocompleteFromCustomValue(
|
||||
keysAutocomplete,
|
||||
fieldKey,
|
||||
dataType,
|
||||
);
|
||||
|
||||
const nextQuery: Query = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
groupBy: [...item.groupBy, existAutocompleteKey],
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
redirectWithQueryBuilderData(nextQuery);
|
||||
} catch {
|
||||
notifications.error({ message: SOMETHING_WENT_WRONG });
|
||||
}
|
||||
},
|
||||
[currentQuery, notifications, queryClient, redirectWithQueryBuilderData],
|
||||
);
|
||||
|
||||
return {
|
||||
activeLog,
|
||||
onSetActiveLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery,
|
||||
onAddToQuery: onAddToQueryExplorer,
|
||||
onGroupByAttribute,
|
||||
};
|
||||
};
|
||||
|
||||
24
frontend/src/lib/logs/generateFilterQuery.ts
Normal file
24
frontend/src/lib/logs/generateFilterQuery.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { QueryOperatorsMultiVal } from 'lib/logql/tokens';
|
||||
|
||||
type Keys = keyof typeof QueryOperatorsMultiVal;
|
||||
type Values = (typeof QueryOperatorsMultiVal)[Keys];
|
||||
|
||||
interface GenerateFilterQueryParams {
|
||||
fieldKey: string;
|
||||
fieldValue: string;
|
||||
type: Values;
|
||||
}
|
||||
export const generateFilterQuery = ({
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
type,
|
||||
}: GenerateFilterQueryParams): string => {
|
||||
let generatedQueryString = `${fieldKey} ${type} `;
|
||||
if (typeof fieldValue === 'number') {
|
||||
generatedQueryString += `(${fieldValue})`;
|
||||
} else {
|
||||
generatedQueryString += `('${fieldValue}')`;
|
||||
}
|
||||
|
||||
return generatedQueryString;
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { DrawerWrapper } from '@signozhq/ui/drawer';
|
||||
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import PanelTypeBrowser from '../../../PanelsAndSectionsLayout/Panel/PanelTypeSelectionModal/PanelTypeBrowser';
|
||||
import PanelTypeBrowser from '../../../PanelsAndSectionsLayout/Panel/PanelTypeSelectionModal/components/PanelTypeBrowser/PanelTypeBrowser';
|
||||
import { getPanelDefinition } from '../../../Panels/registry';
|
||||
import type { PanelKind } from '../../../Panels/types/panelKind';
|
||||
import { JS_DRIVEN_SLIDE } from '../../../utils/drawerSlide';
|
||||
|
||||
@@ -217,6 +217,8 @@ function ListPanelRenderer({
|
||||
log={logDetail.activeLog}
|
||||
selectedTab={logDetail.selectedTab}
|
||||
onClose={logDetail.onClose}
|
||||
onAddToQuery={logDetail.onAddToQuery}
|
||||
onClickActionItem={logDetail.onAddToQuery}
|
||||
onNavigateLog={logDetail.onNavigateLog}
|
||||
logs={logDetail.logs}
|
||||
isListViewPanel
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface ListLogDetail {
|
||||
selectedTab: VIEWS;
|
||||
logs: ILog[];
|
||||
onClose: () => void;
|
||||
onAddToQuery: ReturnType<typeof useLogDetailHandlers>['onAddToQuery'];
|
||||
onNavigateLog: (log: ILog) => void;
|
||||
}
|
||||
|
||||
@@ -48,8 +49,13 @@ export function useListRowInteraction({
|
||||
const isLogs = signal === TelemetrytypesSignalDTO.logs;
|
||||
const isTraces = signal === TelemetrytypesSignalDTO.traces;
|
||||
|
||||
const { activeLog, selectedTab, handleSetActiveLog, handleCloseLogDetail } =
|
||||
useLogDetailHandlers();
|
||||
const {
|
||||
activeLog,
|
||||
onAddToQuery,
|
||||
selectedTab,
|
||||
handleSetActiveLog,
|
||||
handleCloseLogDetail,
|
||||
} = useLogDetailHandlers();
|
||||
|
||||
const logs = useMemo(
|
||||
() => (isLogs ? rows.map(rawRowToILog) : []),
|
||||
@@ -84,6 +90,7 @@ export function useListRowInteraction({
|
||||
selectedTab: selectedTab ?? VIEW_TYPES.OVERVIEW,
|
||||
logs,
|
||||
onClose: handleCloseLogDetail,
|
||||
onAddToQuery,
|
||||
onNavigateLog: handleSetActiveLog,
|
||||
}
|
||||
: null,
|
||||
@@ -93,6 +100,7 @@ export function useListRowInteraction({
|
||||
selectedTab,
|
||||
logs,
|
||||
handleCloseLogDetail,
|
||||
onAddToQuery,
|
||||
handleSetActiveLog,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -7,11 +7,11 @@ import { releasePanelPickerTarget } from '../../../store/usePanelPickerTargetSto
|
||||
import type { NewPanelTarget } from '../../../patchOps';
|
||||
import type { PanelKind } from '../../../Panels/types/panelKind';
|
||||
import { JS_DRIVEN_SLIDE } from '../../../utils/drawerSlide';
|
||||
import AddPanelSplitButton from './AddPanelSplitButton';
|
||||
import NewSectionNameInput from './NewSectionNameInput';
|
||||
import PanelTypeBrowser from './PanelTypeBrowser';
|
||||
import { usePanelPickerDraftSection } from './usePanelPickerDraftSection';
|
||||
import { usePanelPickerTarget } from './usePanelPickerTarget';
|
||||
import AddPanelSplitButton from './components/AddPanelSplitButton/AddPanelSplitButton';
|
||||
import NewSectionNameInput from './components/NewSectionNameInput/NewSectionNameInput';
|
||||
import PanelTypeBrowser from './components/PanelTypeBrowser/PanelTypeBrowser';
|
||||
import { usePanelPickerDraftSection } from './hooks/usePanelPickerDraftSection';
|
||||
import { usePanelPickerTarget } from './hooks/usePanelPickerTarget';
|
||||
import { buildSectionOptions, resolveDefaultSectionValue } from './utils';
|
||||
|
||||
import styles from './PanelTypeSelectionModal.module.scss';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ChevronUp, Plus } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Dropdown, type DropdownItemType } from '@signozhq/ui/dropdown';
|
||||
|
||||
import type { SectionOption } from './types';
|
||||
import type { SectionOption } from '../../types';
|
||||
|
||||
import styles from './AddPanelSplitButton.module.scss';
|
||||
|
||||
@@ -88,76 +88,6 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 6px;
|
||||
background: var(--l2-background);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 200ms cubic-bezier(0.08, 0.52, 0.52, 1),
|
||||
border-color 200ms cubic-bezier(0.08, 0.52, 0.52, 1);
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
.tileSelected {
|
||||
border-color: var(--bg-robin-500);
|
||||
box-shadow: inset 0 0 0 1px var(--bg-robin-500);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
.tileDisabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background: var(--l2-background);
|
||||
}
|
||||
}
|
||||
|
||||
.tileText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.tileTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.tileName {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 13.5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.newBadge {
|
||||
padding: 2px 6px;
|
||||
border: 1px solid var(--bg-sienna-400);
|
||||
border-radius: 3px;
|
||||
color: var(--bg-sienna-400);
|
||||
font-size: 9.5px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.6px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.tileDescription {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -4,12 +4,12 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import cx from 'classnames';
|
||||
|
||||
import type { PanelKind } from '../../../Panels/types/panelKind';
|
||||
import type { PanelKind } from '../../../../../Panels/types/panelKind';
|
||||
import {
|
||||
filterPanelTypeGroups,
|
||||
type PanelTypeGroupId,
|
||||
} from './panelTypeCatalog';
|
||||
import PanelTypeTile from './PanelTypeTile';
|
||||
import PanelTypeTile from '../PanelTypeTile/PanelTypeTile';
|
||||
|
||||
import styles from './PanelTypeBrowser.module.scss';
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { PANEL_OPTIONS, type PanelOption } from '../../../Panels/registry';
|
||||
import type { PanelKind } from '../../../Panels/types/panelKind';
|
||||
import {
|
||||
PANEL_OPTIONS,
|
||||
type PanelOption,
|
||||
} from '../../../../../Panels/registry';
|
||||
import type { PanelKind } from '../../../../../Panels/types/panelKind';
|
||||
|
||||
export type PanelTypeGroupId =
|
||||
| 'trends'
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PanelKind } from '../../../Panels/types/panelKind';
|
||||
import type { PanelKind } from '../../../../../Panels/types/panelKind';
|
||||
import { PANEL_TYPE_PREVIEWS } from './panelTypePreviews';
|
||||
|
||||
import styles from './PanelTypePreview.module.scss';
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PanelKind } from '../../../Panels/types/panelKind';
|
||||
import type { PanelKind } from '../../../../../Panels/types/panelKind';
|
||||
import PreviewBars from './PreviewBars';
|
||||
import PreviewRows, { type PreviewCell } from './PreviewRows';
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
.tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 6px;
|
||||
background: var(--l2-background);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 200ms cubic-bezier(0.08, 0.52, 0.52, 1),
|
||||
border-color 200ms cubic-bezier(0.08, 0.52, 0.52, 1);
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
.tileSelected {
|
||||
border-color: var(--bg-robin-500);
|
||||
box-shadow: inset 0 0 0 1px var(--bg-robin-500);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
.tileDisabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background: var(--l2-background);
|
||||
}
|
||||
}
|
||||
|
||||
.tileText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.tileTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.tileName {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 13.5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.newBadge {
|
||||
padding: 2px 6px;
|
||||
border: 1px solid var(--bg-sienna-400);
|
||||
border-radius: 3px;
|
||||
color: var(--bg-sienna-400);
|
||||
font-size: 9.5px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.6px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.tileDescription {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import cx from 'classnames';
|
||||
|
||||
import type { PanelTypeItem } from './panelTypeCatalog';
|
||||
import PanelTypePreview from './PanelTypePreview';
|
||||
import type { PanelTypeItem } from '../PanelTypeBrowser/panelTypeCatalog';
|
||||
import PanelTypePreview from '../PanelTypePreview/PanelTypePreview';
|
||||
|
||||
import styles from './PanelTypeBrowser.module.scss';
|
||||
import styles from './PanelTypeTile.module.scss';
|
||||
|
||||
interface PanelTypeTileProps {
|
||||
item: PanelTypeItem;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import type { PanelKind } from '../../../Panels/types/panelKind';
|
||||
import { usePanelPickerTargetStore } from '../../../store/usePanelPickerTargetStore';
|
||||
import type { PanelKind } from '../../../../Panels/types/panelKind';
|
||||
import { usePanelPickerTargetStore } from '../../../../store/usePanelPickerTargetStore';
|
||||
|
||||
export function usePanelPickerDraftSection(
|
||||
title: string | null,
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import type { PanelKind } from '../../../Panels/types/panelKind';
|
||||
import { usePanelPickerTargetStore } from '../../../store/usePanelPickerTargetStore';
|
||||
import type { PanelKind } from '../../../../Panels/types/panelKind';
|
||||
import { usePanelPickerTargetStore } from '../../../../store/usePanelPickerTargetStore';
|
||||
|
||||
interface UsePanelPickerTargetArgs {
|
||||
open: boolean;
|
||||
Reference in New Issue
Block a user