Save View for Explorer pages. (#3404)

* feat: save view switch and save view done

* feat: delete view completed

* refactor: moved update logic to utils

* chore: removed unwated commented logic

* refactor: shifted save view logic to utils

* refactor: separated types

* refactor: updated types for save view

* refactor: shifted delete view logic to utils

* refactor: done with share url

* refactor: separated constants

* refactor: separated types

* test: added unit test for explorerCard

* refactor: done with update view

* refactor: added test cases

* chore: updated the file name from index to ExplorerCard

* refactor: moved unit test to test folder and useCallbacks

* chore: changed the variable names

* refactor: updated code review comments

* chore: fix build pipeline

* fix: 404 for query_range because of attribute operator

* refactor: functional review commnet address

* refactor: updatd unit test

* refactor: added delete option beside save view

* refactor: row align middle

* fix: build pipeline

* refactor: updated logic and review comments changes

* refactor: fixed build pipeline

* refactor: used onSuccess and onError for mutation

* refactor: onSuccess and onError for saveView

* refactor: mapping in function with query type

* refactor: updated code review comments

* refactor: updated explorerCard utils

* refactor: removed async

* fix: update state for save view

* refactor: tab according to aggregate operator

* refactor: updated test case

* refactor: updated the loading state of the button

* fix: build pipeline

* fix: share view tab updates

* fix: click on dropdown

---------

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
Co-authored-by: Yunus M <myounis.ar@live.com>
This commit is contained in:
Rajat Dabade 2023-08-30 20:24:16 +05:30 committed by GitHub
parent 15f328eb9e
commit 1138c6e41a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1383 additions and 126 deletions

View File

@ -0,0 +1,5 @@
import axios from 'api';
import { DeleteViewPayloadProps } from 'types/api/saveViews/types';
export const deleteView = (uuid: string): Promise<DeleteViewPayloadProps> =>
axios.delete(`explorer/views/${uuid}`);

View File

@ -0,0 +1,9 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import { AllViewsProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
export const getAllViews = (
sourcepage: DataSource,
): Promise<AxiosResponse<AllViewsProps>> =>
axios.get(`explorer/views?sourcePage=${sourcepage}`);

View File

@ -0,0 +1,16 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
export const saveView = ({
compositeQuery,
sourcePage,
viewName,
extraData,
}: SaveViewProps): Promise<AxiosResponse<SaveViewPayloadProps>> =>
axios.post('explorer/views', {
name: viewName,
sourcePage,
compositeQuery,
extraData,
});

View File

@ -0,0 +1,19 @@
import axios from 'api';
import {
UpdateViewPayloadProps,
UpdateViewProps,
} from 'types/api/saveViews/types';
export const updateView = ({
compositeQuery,
viewName,
extraData,
sourcePage,
viewKey,
}: UpdateViewProps): Promise<UpdateViewPayloadProps> =>
axios.put(`explorer/views/${viewKey}`, {
name: viewName,
compositeQuery,
extraData,
sourcePage,
});

View File

@ -0,0 +1,278 @@
import {
DeleteOutlined,
DownOutlined,
MoreOutlined,
SaveOutlined,
ShareAltOutlined,
} from '@ant-design/icons';
import {
Button,
Card,
Col,
Dropdown,
MenuProps,
Popover,
Row,
Space,
Typography,
} from 'antd';
import axios from 'axios';
import TextToolTip from 'components/TextToolTip';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { querySearchParams } from 'constants/queryBuilderQueryNames';
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
import { useUpdateView } from 'hooks/saveViews/useUpdateView';
import useErrorNotification from 'hooks/useErrorNotification';
import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { ExploreHeaderToolTip, SaveButtonText } from './constants';
import MenuItemGenerator from './MenuItemGenerator';
import SaveViewWithName from './SaveViewWithName';
import {
DropDownOverlay,
ExplorerCardHeadContainer,
OffSetCol,
} from './styles';
import { ExplorerCardProps } from './types';
import { deleteViewHandler, isQueryUpdatedInView } from './utils';
function ExplorerCard({
sourcepage,
children,
}: ExplorerCardProps): JSX.Element {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [, setCopyUrl] = useCopyToClipboard();
const [isQueryUpdated, setIsQueryUpdated] = useState<boolean>(false);
const { notifications } = useNotifications();
const onCopyUrlHandler = (): void => {
setCopyUrl(window.location.href);
notifications.success({
message: 'Copied to clipboard',
});
};
const {
stagedQuery,
currentQuery,
panelType,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const {
data: viewsData,
isLoading,
error,
isRefetching,
refetch: refetchAllView,
} = useGetAllViews(sourcepage);
useErrorNotification(error);
const handlePopOverClose = (): void => {
setIsOpen(false);
};
const handleOpenChange = (newOpen: boolean): void => {
setIsOpen(newOpen);
};
const viewName =
useGetSearchQueryParam(querySearchParams.viewName) || 'Query Builder';
const viewKey = useGetSearchQueryParam(querySearchParams.viewKey) || '';
const { mutateAsync: updateViewAsync } = useUpdateView({
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
viewKey,
extraData: '',
sourcePage: sourcepage,
viewName,
});
const { mutateAsync: deleteViewAsync } = useDeleteView(viewKey);
const showErrorNotification = (err: Error): void => {
notifications.error({
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
});
};
const onDeleteHandler = useCallback(() => {
deleteViewHandler({
deleteViewAsync,
notifications,
panelType,
redirectWithQueryBuilderData,
refetchAllView,
viewId: viewKey,
viewKey,
});
}, [
deleteViewAsync,
notifications,
panelType,
redirectWithQueryBuilderData,
refetchAllView,
viewKey,
]);
const onUpdateQueryHandler = (): void => {
updateViewAsync(
{
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
viewKey,
extraData: '',
sourcePage: sourcepage,
viewName,
},
{
onSuccess: () => {
setIsQueryUpdated(false);
notifications.success({
message: 'View Updated Successfully',
});
refetchAllView();
},
onError: (err) => {
showErrorNotification(err);
},
},
);
};
useEffect(() => {
setIsQueryUpdated(
isQueryUpdatedInView({
data: viewsData?.data?.data,
stagedQuery,
viewKey,
currentPanelType: panelType,
}),
);
}, [
currentQuery,
viewsData?.data?.data,
stagedQuery,
stagedQuery?.builder.queryData,
viewKey,
panelType,
]);
const menu = useMemo(
(): MenuProps => ({
items: viewsData?.data?.data?.map((view) => ({
key: view.uuid,
label: (
<MenuItemGenerator
viewName={view.name}
viewKey={viewKey}
createdBy={view.createdBy}
uuid={view.uuid}
refetchAllView={refetchAllView}
viewData={viewsData.data.data}
/>
),
})),
}),
[refetchAllView, viewKey, viewsData?.data?.data],
);
const moreOptionMenu = useMemo(
(): MenuProps => ({
items: [
{
key: 'delete',
label: <Typography.Text strong>Delete</Typography.Text>,
onClick: onDeleteHandler,
icon: <DeleteOutlined />,
},
],
}),
[onDeleteHandler],
);
const saveButtonType = isQueryUpdated ? 'default' : 'primary';
const saveButtonIcon = isQueryUpdated ? null : <SaveOutlined />;
return (
<>
<ExplorerCardHeadContainer size="small">
<Row align="middle">
<Col span={6}>
<Space>
<Typography>{viewName}</Typography>
<TextToolTip
url={ExploreHeaderToolTip.url}
text={ExploreHeaderToolTip.text}
useFilledIcon={false}
/>
</Space>
</Col>
<OffSetCol span={10} offset={8}>
<Space size="large">
{viewsData?.data.data && viewsData?.data.data.length && (
<Space>
{/* <Typography.Text>Saved Views</Typography.Text> */}
<Dropdown.Button
menu={menu}
loading={isLoading || isRefetching}
icon={<DownOutlined />}
trigger={['click']}
overlayStyle={DropDownOverlay}
>
Select View
</Dropdown.Button>
</Space>
)}
{isQueryUpdated && (
<Button
type="primary"
icon={<SaveOutlined />}
onClick={onUpdateQueryHandler}
>
Save changes
</Button>
)}
<Popover
placement="bottomLeft"
trigger="click"
content={
<SaveViewWithName
sourcePage={sourcepage}
handlePopOverClose={handlePopOverClose}
refetchAllView={refetchAllView}
/>
}
showArrow={false}
open={isOpen}
onOpenChange={handleOpenChange}
>
<Button type={saveButtonType} icon={saveButtonIcon}>
{isQueryUpdated
? SaveButtonText.SAVE_AS_NEW_VIEW
: SaveButtonText.SAVE_VIEW}
</Button>
</Popover>
<ShareAltOutlined onClick={onCopyUrlHandler} />
{viewKey && (
<Dropdown trigger={['click']} menu={moreOptionMenu}>
<MoreOutlined />
</Dropdown>
)}
</Space>
</OffSetCol>
</Row>
</ExplorerCardHeadContainer>
<Card>{children}</Card>
</>
);
}
export default ExplorerCard;

View File

@ -0,0 +1,87 @@
import { DeleteOutlined } from '@ant-design/icons';
import { Col, Row, Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { MouseEvent, useCallback } from 'react';
import { MenuItemContainer } from './styles';
import { MenuItemLabelGeneratorProps } from './types';
import { deleteViewHandler, getViewDetailsUsingViewKey } from './utils';
function MenuItemGenerator({
viewName,
viewKey,
createdBy,
uuid,
viewData,
refetchAllView,
}: MenuItemLabelGeneratorProps): JSX.Element {
const { panelType, redirectWithQueryBuilderData } = useQueryBuilder();
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const { notifications } = useNotifications();
const { mutateAsync: deleteViewAsync } = useDeleteView(uuid);
const onDeleteHandler = (event: MouseEvent<HTMLElement>): void => {
event.stopPropagation();
deleteViewHandler({
deleteViewAsync,
notifications,
panelType,
redirectWithQueryBuilderData,
refetchAllView,
viewId: uuid,
viewKey,
});
};
const onMenuItemSelectHandler = useCallback(
({ key }: { key: string }): void => {
const currentViewDetails = getViewDetailsUsingViewKey(key, viewData);
if (!currentViewDetails) return;
const {
query,
name,
uuid,
panelType: currentPanelType,
} = currentViewDetails;
handleExplorerTabChange(currentPanelType, {
query,
name,
uuid,
});
},
[viewData, handleExplorerTabChange],
);
const onLabelClickHandler = (): void => {
onMenuItemSelectHandler({
key: uuid,
});
};
return (
<MenuItemContainer onClick={onLabelClickHandler}>
<Row justify="space-between">
<Col span={22}>
<Row>
<Typography.Text strong>{viewName}</Typography.Text>
</Row>
<Row>
<Typography.Text type="secondary">Created by {createdBy}</Typography.Text>
</Row>
</Col>
<Col span={2}>
<Typography.Link>
<DeleteOutlined onClick={onDeleteHandler} />
</Typography.Link>
</Col>
</Row>
</MenuItemContainer>
);
}
export default MenuItemGenerator;

View File

@ -0,0 +1,68 @@
import { Card, Input, Typography } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSaveView } from 'hooks/saveViews/useSaveView';
import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { ChangeEvent, useCallback, useState } from 'react';
import { SaveButton } from './styles';
import { SaveViewWithNameProps } from './types';
import { saveViewHandler } from './utils';
function SaveViewWithName({
sourcePage,
handlePopOverClose,
refetchAllView,
}: SaveViewWithNameProps): JSX.Element {
const [name, setName] = useState('');
const {
currentQuery,
panelType,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const { notifications } = useNotifications();
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
const { isLoading, mutateAsync: saveViewAsync } = useSaveView({
viewName: name,
compositeQuery,
sourcePage,
extraData: '',
});
const onChangeHandler = useCallback(
(e: ChangeEvent<HTMLInputElement>): void => {
setName(e.target.value);
},
[],
);
const onSaveHandler = (): void => {
saveViewHandler({
compositeQuery,
handlePopOverClose,
extraData: '',
notifications,
panelType: panelType || PANEL_TYPES.LIST,
redirectWithQueryBuilderData,
refetchAllView,
saveViewAsync,
sourcePage,
viewName: name,
setName,
});
};
return (
<Card>
<Typography>Name of the View</Typography>
<Input placeholder="Enter Name" onChange={onChangeHandler} />
<SaveButton onClick={onSaveHandler} type="primary" loading={isLoading}>
Save
</SaveButton>
</Card>
);
}
export default SaveViewWithName;

View File

@ -0,0 +1,32 @@
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { ViewProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
export const viewMockData: ViewProps[] = [
{
uuid: 'view1',
name: 'View 1',
createdBy: 'User 1',
category: 'category 1',
compositeQuery: {} as ICompositeMetricQuery,
createdAt: '2021-07-07T06:31:00.000Z',
updatedAt: '2021-07-07T06:33:00.000Z',
extraData: '',
sourcePage: DataSource.TRACES,
tags: [],
updatedBy: 'User 1',
},
{
uuid: 'view2',
name: 'View 2',
createdBy: 'User 2',
category: 'category 2',
compositeQuery: {} as ICompositeMetricQuery,
createdAt: '2021-07-07T06:30:00.000Z',
updatedAt: '2021-07-07T06:30:00.000Z',
extraData: '',
sourcePage: DataSource.TRACES,
tags: [],
updatedBy: 'User 2',
},
];

View File

@ -0,0 +1,10 @@
export const ExploreHeaderToolTip = {
url:
'https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=new-query-builder',
text: 'More details on how to use query builder',
};
export const SaveButtonText = {
SAVE_AS_NEW_VIEW: 'Save as new view',
SAVE_VIEW: 'Save view',
};

View File

@ -1,27 +0,0 @@
import { Card, Space, Typography } from 'antd';
import TextToolTip from 'components/TextToolTip';
function ExplorerCard({ children }: Props): JSX.Element {
return (
<Card
size="small"
title={
<Space>
<Typography>Query Builder</Typography>
<TextToolTip
url="https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=new-query-builder"
text="More details on how to use query builder"
/>
</Space>
}
>
{children}
</Card>
);
}
interface Props {
children: React.ReactNode;
}
export default ExplorerCard;

View File

@ -0,0 +1,28 @@
import { Button, Card, Col } from 'antd';
import styled, { CSSProperties } from 'styled-components';
export const ExplorerCardHeadContainer = styled(Card)`
margin: 1rem 0;
`;
export const OffSetCol = styled(Col)`
text-align: right;
`;
export const SaveButton = styled(Button)`
&&& {
margin: 1rem 0;
width: 5rem;
}
`;
export const DropDownOverlay: CSSProperties = {
maxHeight: '20rem',
overflowY: 'auto',
width: '20rem',
padding: 0,
};
export const MenuItemContainer = styled(Card)`
padding: 0;
`;

View File

@ -0,0 +1,76 @@
import { fireEvent, render, screen } from '@testing-library/react';
import ROUTES from 'constants/routes';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { DataSource } from 'types/common/queryBuilder';
import { viewMockData } from '../__mock__/viewData';
import ExplorerCard from '../ExplorerCard';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
}),
}));
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
}));
jest.mock('hooks/saveViews/useGetAllViews', () => ({
useGetAllViews: jest.fn(() => ({
data: { data: { data: viewMockData } },
isLoading: false,
error: null,
isRefetching: false,
refetch: jest.fn(),
})),
}));
jest.mock('hooks/saveViews/useUpdateView', () => ({
useUpdateView: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
jest.mock('hooks/saveViews/useDeleteView', () => ({
useDeleteView: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
describe('ExplorerCard', () => {
it('renders a card with a title and a description', () => {
render(
<MockQueryClientProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</MockQueryClientProvider>,
);
expect(screen.getByText('Query Builder')).toBeInTheDocument();
});
it('renders a save view button', () => {
render(
<MockQueryClientProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</MockQueryClientProvider>,
);
expect(screen.getByText('Save view')).toBeInTheDocument();
});
it('should see all the view listed in dropdown', async () => {
const screen = render(
<ExplorerCard sourcepage={DataSource.TRACES}>Mock Children</ExplorerCard>,
);
const selectButton = screen.getByText('Select View');
fireEvent.click(selectButton);
const spanElement = screen.getByRole('img', {
name: 'down',
});
fireEvent.click(spanElement);
const viewNameText = await screen.findByText('View 2');
expect(viewNameText).toBeInTheDocument();
});
});

View File

@ -0,0 +1,53 @@
import { render, screen } from '@testing-library/react';
import ROUTES from 'constants/routes';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { viewMockData } from '../__mock__/viewData';
import MenuItemGenerator from '../MenuItemGenerator';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
}),
}));
describe('MenuItemGenerator', () => {
it('should render MenuItemGenerator component', () => {
const screen = render(
<MockQueryClientProvider>
<MenuItemGenerator
viewName={viewMockData[0].name}
viewKey={viewMockData[0].uuid}
createdBy={viewMockData[0].createdBy}
uuid={viewMockData[0].uuid}
refetchAllView={jest.fn()}
viewData={viewMockData}
/>
</MockQueryClientProvider>,
);
expect(screen.getByText(viewMockData[0].name)).toBeInTheDocument();
});
it('should call onMenuItemSelectHandler on click of MenuItemGenerator', () => {
render(
<MockQueryClientProvider>
<MenuItemGenerator
viewName={viewMockData[0].name}
viewKey={viewMockData[0].uuid}
createdBy={viewMockData[0].createdBy}
uuid={viewMockData[0].uuid}
refetchAllView={jest.fn()}
viewData={viewMockData}
/>
</MockQueryClientProvider>,
);
const spanElement = screen.getByRole('img', {
name: 'delete',
});
expect(spanElement).toBeInTheDocument();
});
});

View File

@ -0,0 +1,63 @@
import { fireEvent, render } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { QueryClient, QueryClientProvider } from 'react-query';
import { DataSource } from 'types/common/queryBuilder';
import SaveViewWithName from '../SaveViewWithName';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
}),
}));
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
}));
jest.mock('hooks/saveViews/useSaveView', () => ({
useSaveView: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
describe('SaveViewWithName', () => {
it('should render SaveViewWithName component', () => {
const screen = render(
<QueryClientProvider client={queryClient}>
<SaveViewWithName
sourcePage={DataSource.TRACES}
handlePopOverClose={jest.fn()}
refetchAllView={jest.fn()}
/>
</QueryClientProvider>,
);
expect(screen.getByText('Save')).toBeInTheDocument();
});
it('should call saveViewAsync on click of Save button', () => {
const screen = render(
<QueryClientProvider client={queryClient}>
<SaveViewWithName
sourcePage={DataSource.TRACES}
handlePopOverClose={jest.fn()}
refetchAllView={jest.fn()}
/>
</QueryClientProvider>,
);
fireEvent.click(screen.getByText('Save'));
expect(screen.getByText('Save')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,77 @@
import { NotificationInstance } from 'antd/es/notification/interface';
import { AxiosResponse } from 'axios';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { SetStateAction } from 'react';
import { UseMutateAsyncFunction } from 'react-query';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
DeleteViewPayloadProps,
SaveViewPayloadProps,
SaveViewProps,
ViewProps,
} from 'types/api/saveViews/types';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
export interface ExplorerCardProps {
sourcepage: DataSource;
children: React.ReactNode;
}
export type GetViewDetailsUsingViewKey = (
viewKey: string,
data: ViewProps[] | undefined,
) =>
| { query: Query; name: string; uuid: string; panelType: PANEL_TYPES }
| undefined;
export interface IsQueryUpdatedInViewProps {
viewKey: string;
data: ViewProps[] | undefined;
stagedQuery: Query | null;
currentPanelType: PANEL_TYPES | null;
}
export interface SaveViewWithNameProps {
sourcePage: ExplorerCardProps['sourcepage'];
handlePopOverClose: VoidFunction;
refetchAllView: VoidFunction;
}
export interface MenuItemLabelGeneratorProps {
viewName: string;
viewKey: string;
createdBy: string;
uuid: string;
viewData: ViewProps[];
refetchAllView: VoidFunction;
}
export interface SaveViewHandlerProps {
viewName: string;
compositeQuery: ICompositeMetricQuery;
sourcePage: ExplorerCardProps['sourcepage'];
extraData: string;
panelType: PANEL_TYPES | null;
notifications: NotificationInstance;
refetchAllView: SaveViewWithNameProps['refetchAllView'];
saveViewAsync: UseMutateAsyncFunction<
AxiosResponse<SaveViewPayloadProps>,
Error,
SaveViewProps,
SaveViewPayloadProps
>;
handlePopOverClose: SaveViewWithNameProps['handlePopOverClose'];
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
setName: (value: SetStateAction<string>) => void;
}
export interface DeleteViewHandlerProps {
deleteViewAsync: UseMutateAsyncFunction<DeleteViewPayloadProps, Error, string>;
refetchAllView: MenuItemLabelGeneratorProps['refetchAllView'];
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
notifications: NotificationInstance;
panelType: PANEL_TYPES | null;
viewKey: string;
viewId: string;
}

View File

@ -0,0 +1,170 @@
import { NotificationInstance } from 'antd/es/notification/interface';
import axios from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { initialQueriesMap } from 'constants/queryBuilder';
import {
queryParamNamesMap,
querySearchParams,
} from 'constants/queryBuilderQueryNames';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import isEqual from 'lodash-es/isEqual';
import {
DeleteViewHandlerProps,
GetViewDetailsUsingViewKey,
IsQueryUpdatedInViewProps,
SaveViewHandlerProps,
} from './types';
const showErrorNotification = (
notifications: NotificationInstance,
err: Error,
): void => {
notifications.error({
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
});
};
export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
viewKey,
data,
) => {
const selectedView = data?.find((view) => view.uuid === viewKey);
if (selectedView) {
const { compositeQuery, name, uuid } = selectedView;
const query = mapQueryDataFromApi(compositeQuery);
return { query, name, uuid, panelType: compositeQuery.panelType };
}
return undefined;
};
export const isQueryUpdatedInView = ({
viewKey,
data,
stagedQuery,
currentPanelType,
}: IsQueryUpdatedInViewProps): boolean => {
const currentViewDetails = getViewDetailsUsingViewKey(viewKey, data);
if (!currentViewDetails) {
return false;
}
const { query, panelType } = currentViewDetails;
// Omitting id from aggregateAttribute and groupBy
const updatedCurrentQuery = {
...stagedQuery,
builder: {
...stagedQuery?.builder,
queryData: stagedQuery?.builder.queryData.map((queryData) => {
const { id, ...rest } = queryData.aggregateAttribute;
const newAggregateAttribute = rest;
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
const { id, ...rest } = groupByAttribute;
return rest;
});
const newItems = queryData.filters.items.map((item) => {
const { id, ...newItem } = item;
if (item.key) {
const { id, ...rest } = item.key;
return {
...newItem,
key: rest,
};
}
return newItem;
});
return {
...queryData,
aggregateAttribute: newAggregateAttribute,
groupBy: newGroupByAttributes,
filters: {
...queryData.filters,
items: newItems,
},
limit: queryData.limit ? queryData.limit : 0,
offset: queryData.offset ? queryData.offset : 0,
pageSize: queryData.pageSize ? queryData.pageSize : 0,
};
}),
},
};
return (
panelType !== currentPanelType ||
!isEqual(query.builder, updatedCurrentQuery?.builder) ||
!isEqual(query.clickhouse_sql, updatedCurrentQuery?.clickhouse_sql) ||
!isEqual(query.promql, updatedCurrentQuery?.promql)
);
};
export const saveViewHandler = ({
saveViewAsync,
refetchAllView,
notifications,
handlePopOverClose,
viewName,
compositeQuery,
sourcePage,
extraData,
redirectWithQueryBuilderData,
panelType,
setName,
}: SaveViewHandlerProps): void => {
saveViewAsync(
{
viewName,
compositeQuery,
sourcePage,
extraData,
},
{
onSuccess: (data) => {
refetchAllView();
redirectWithQueryBuilderData(mapQueryDataFromApi(compositeQuery), {
[queryParamNamesMap.panelTypes]: panelType,
[querySearchParams.viewName]: viewName,
[querySearchParams.viewKey]: data.data.data,
});
notifications.success({
message: 'View Saved Successfully',
});
},
onError: (err) => {
showErrorNotification(notifications, err);
},
onSettled: () => {
handlePopOverClose();
setName('');
},
},
);
};
export const deleteViewHandler = ({
deleteViewAsync,
refetchAllView,
redirectWithQueryBuilderData,
notifications,
panelType,
viewKey,
viewId,
}: DeleteViewHandlerProps): void => {
deleteViewAsync(viewKey, {
onSuccess: () => {
if (viewId === viewKey) {
redirectWithQueryBuilderData(initialQueriesMap.traces, {
[querySearchParams.viewName]: 'Query Builder',
[queryParamNamesMap.panelTypes]: panelType,
[querySearchParams.viewKey]: '',
});
}
notifications.success({
message: 'View Deleted Successfully',
});
refetchAllView();
},
onError: (err) => {
showErrorNotification(notifications, err);
},
});
};

View File

@ -6,6 +6,8 @@ type QueryParamNames =
| 'selectedFields'
| 'linesPerRow';
export type QuerySearchParamNames = 'viewName' | 'viewKey';
export const queryParamNamesMap: Record<QueryParamNames, QueryParamNames> = {
compositeQuery: 'compositeQuery',
panelTypes: 'panelTypes',
@ -14,3 +16,11 @@ export const queryParamNamesMap: Record<QueryParamNames, QueryParamNames> = {
selectedFields: 'selectedFields',
linesPerRow: 'linesPerRow',
};
export const querySearchParams: Record<
QuerySearchParamNames,
QuerySearchParamNames
> = {
viewName: 'viewName',
viewKey: 'viewKey',
};

View File

@ -2,7 +2,6 @@ import { Tabs, TabsProps } from 'antd';
import TabLabel from 'components/TabLabel';
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
import {
initialAutocompleteData,
initialFilters,
initialQueriesMap,
initialQueryBuilderFormValues,
@ -15,7 +14,6 @@ import GoToTop from 'container/GoToTop';
import LogsExplorerChart from 'container/LogsExplorerChart';
import LogsExplorerList from 'container/LogsExplorerList';
import LogsExplorerTable from 'container/LogsExplorerTable';
import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/constants';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
@ -24,6 +22,7 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useAxiosError from 'hooks/useAxiosError';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
@ -67,10 +66,10 @@ function LogsExplorerViews(): JSX.Element {
stagedQuery,
panelType,
updateAllQueriesOperators,
updateQueriesData,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const { handleExplorerTabChange } = useHandleExplorerTabChange();
// State
const [page, setPage] = useState<number>(1);
const [logs, setLogs] = useState<ILog[]>([]);
@ -172,42 +171,6 @@ function LogsExplorerViews(): JSX.Element {
},
);
const getUpdateQuery = useCallback(
(newPanelType: PANEL_TYPES): Query => {
let query = updateAllQueriesOperators(
currentQuery,
newPanelType,
DataSource.TRACES,
);
if (newPanelType === PANEL_TYPES.LIST) {
query = updateQueriesData(query, 'queryData', (item) => ({
...item,
orderBy: item.orderBy.filter((item) => item.columnName !== SIGNOZ_VALUE),
aggregateAttribute: initialAutocompleteData,
}));
}
return query;
},
[currentQuery, updateAllQueriesOperators, updateQueriesData],
);
const handleChangeView = useCallback(
(type: string) => {
const newPanelType = type as PANEL_TYPES;
if (newPanelType === panelType) return;
const query = getUpdateQuery(newPanelType);
redirectWithQueryBuilderData(query, {
[queryParamNamesMap.panelTypes]: newPanelType,
});
},
[panelType, getUpdateQuery, redirectWithQueryBuilderData],
);
const getRequestData = useCallback(
(
query: Query | null,
@ -362,9 +325,9 @@ function LogsExplorerViews(): JSX.Element {
const shouldChangeView = isMultipleQueries || isGroupByExist;
if (panelType === PANEL_TYPES.LIST && shouldChangeView) {
handleChangeView(PANEL_TYPES.TIME_SERIES);
handleExplorerTabChange(PANEL_TYPES.TIME_SERIES);
}
}, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]);
}, [panelType, isMultipleQueries, isGroupByExist, handleExplorerTabChange]);
useEffect(() => {
const currentParams = data?.params as Omit<LogTimeRange, 'pageSize'>;
@ -518,7 +481,7 @@ function LogsExplorerViews(): JSX.Element {
items={tabsItems}
defaultActiveKey={panelType || PANEL_TYPES.LIST}
activeKey={panelType || PANEL_TYPES.LIST}
onChange={handleChangeView}
onChange={handleExplorerTabChange}
destroyInactiveTabPane
/>

View File

@ -0,0 +1,15 @@
import { QuerySearchParamNames } from 'constants/queryBuilderQueryNames';
import useUrlQuery from 'hooks/useUrlQuery';
import { useMemo } from 'react';
export const useGetSearchQueryParam = (
searchParams: QuerySearchParamNames,
): string | null => {
const urlQuery = useUrlQuery();
return useMemo(() => {
const searchQuery = urlQuery.get(searchParams);
return searchQuery ? JSON.parse(searchQuery) : null;
}, [urlQuery, searchParams]);
};

View File

@ -0,0 +1,11 @@
import { deleteView } from 'api/saveView/deleteView';
import { useMutation, UseMutationResult } from 'react-query';
import { DeleteViewPayloadProps } from 'types/api/saveViews/types';
export const useDeleteView = (
uuid: string,
): UseMutationResult<DeleteViewPayloadProps, Error, string> =>
useMutation({
mutationKey: [uuid],
mutationFn: () => deleteView(uuid),
});

View File

@ -0,0 +1,13 @@
import { getAllViews } from 'api/saveView/getAllViews';
import { AxiosError, AxiosResponse } from 'axios';
import { useQuery, UseQueryResult } from 'react-query';
import { AllViewsProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
export const useGetAllViews = (
sourcepage: DataSource,
): UseQueryResult<AxiosResponse<AllViewsProps>, AxiosError> =>
useQuery<AxiosResponse<AllViewsProps>, AxiosError>({
queryKey: [{ sourcepage }],
queryFn: () => getAllViews(sourcepage),
});

View File

@ -0,0 +1,26 @@
import { saveView } from 'api/saveView/saveView';
import { AxiosResponse } from 'axios';
import { useMutation, UseMutationResult } from 'react-query';
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
export const useSaveView = ({
compositeQuery,
sourcePage,
viewName,
extraData,
}: SaveViewProps): UseMutationResult<
AxiosResponse<SaveViewPayloadProps>,
Error,
SaveViewProps,
SaveViewPayloadProps
> =>
useMutation({
mutationKey: [viewName, sourcePage, compositeQuery, extraData],
mutationFn: () =>
saveView({
compositeQuery,
sourcePage,
viewName,
extraData,
}),
});

View File

@ -0,0 +1,30 @@
import { updateView } from 'api/saveView/updateView';
import { useMutation, UseMutationResult } from 'react-query';
import {
UpdateViewPayloadProps,
UpdateViewProps,
} from 'types/api/saveViews/types';
export const useUpdateView = ({
compositeQuery,
viewName,
extraData,
sourcePage,
viewKey,
}: UpdateViewProps): UseMutationResult<
UpdateViewPayloadProps,
Error,
UpdateViewProps,
UpdateViewPayloadProps
> =>
useMutation({
mutationKey: [viewName, sourcePage, compositeQuery, extraData],
mutationFn: () =>
updateView({
compositeQuery,
viewName,
extraData,
sourcePage,
viewKey,
}),
});

View File

@ -0,0 +1,81 @@
import { initialAutocompleteData, PANEL_TYPES } from 'constants/queryBuilder';
import {
queryParamNamesMap,
querySearchParams,
} from 'constants/queryBuilderQueryNames';
import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/constants';
import { useCallback } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { useGetSearchQueryParam } from './queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from './queryBuilder/useQueryBuilder';
export const useHandleExplorerTabChange = (): {
handleExplorerTabChange: (
type: string,
querySearchParameters?: ICurrentQueryData,
) => void;
} => {
const {
currentQuery,
panelType,
redirectWithQueryBuilderData,
updateAllQueriesOperators,
updateQueriesData,
} = useQueryBuilder();
const viewName =
useGetSearchQueryParam(querySearchParams.viewName) || 'Query Builder';
const viewKey = useGetSearchQueryParam(querySearchParams.viewKey) || '';
const getUpdateQuery = useCallback(
(newPanelType: PANEL_TYPES): Query => {
let query = updateAllQueriesOperators(
currentQuery,
newPanelType,
DataSource.TRACES,
);
if (
newPanelType === PANEL_TYPES.LIST ||
newPanelType === PANEL_TYPES.TRACE
) {
query = updateQueriesData(query, 'queryData', (item) => ({
...item,
orderBy: item.orderBy.filter((item) => item.columnName !== SIGNOZ_VALUE),
aggregateAttribute: initialAutocompleteData,
}));
}
return query;
},
[currentQuery, updateAllQueriesOperators, updateQueriesData],
);
const handleExplorerTabChange = useCallback(
(type: string, currentQueryData?: ICurrentQueryData) => {
const newPanelType = type as PANEL_TYPES;
if (newPanelType === panelType && !currentQueryData) return;
const query = currentQueryData?.query || getUpdateQuery(newPanelType);
redirectWithQueryBuilderData(query, {
[queryParamNamesMap.panelTypes]: newPanelType,
[querySearchParams.viewName]: currentQueryData?.name || viewName,
[querySearchParams.viewKey]: currentQueryData?.uuid || viewKey,
});
},
[getUpdateQuery, panelType, redirectWithQueryBuilderData, viewKey, viewName],
);
return { handleExplorerTabChange };
};
interface ICurrentQueryData {
name: string;
uuid: string;
query: Query;
}

View File

@ -0,0 +1,107 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import {
BuilderClickHouseResource,
BuilderPromQLResource,
IClickHouseQuery,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { mapQueryDataToApi } from './mapQueryDataToApi';
const defaultCompositeQuery: ICompositeMetricQuery = {
queryType: EQueryType.QUERY_BUILDER,
panelType: PANEL_TYPES.TIME_SERIES,
builderQueries: {},
chQueries: {},
promQueries: {},
unit: undefined,
};
const buildBuilderQuery = (
query: Query,
panelType: PANEL_TYPES | null,
): ICompositeMetricQuery => {
const { queryData, queryFormulas } = query.builder;
const currentQueryData = mapQueryDataToApi(queryData, 'queryName');
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
const builderQueries = {
...currentQueryData.data,
...currentFormulas.data,
};
const compositeQuery = defaultCompositeQuery;
compositeQuery.queryType = query.queryType;
compositeQuery.panelType = panelType || PANEL_TYPES.TIME_SERIES;
compositeQuery.builderQueries = builderQueries;
return compositeQuery;
};
const buildClickHouseQuery = (
query: Query,
panelType: PANEL_TYPES | null,
): ICompositeMetricQuery => {
const chQueries: BuilderClickHouseResource = {};
query.clickhouse_sql.forEach((query: IClickHouseQuery) => {
if (!query.query) return;
chQueries[query.name] = query;
});
const compositeQuery = defaultCompositeQuery;
compositeQuery.queryType = query.queryType;
compositeQuery.panelType = panelType || PANEL_TYPES.TIME_SERIES;
compositeQuery.chQueries = chQueries;
return compositeQuery;
};
const buildPromQuery = (
query: Query,
panelType: PANEL_TYPES | null,
): ICompositeMetricQuery => {
const promQueries: BuilderPromQLResource = {};
query.promql.forEach((query) => {
if (!query.query) return;
promQueries[query.name] = {
legend: query.legend,
name: query.name,
query: query.query,
disabled: query.disabled,
};
});
const compositeQuery = defaultCompositeQuery;
compositeQuery.queryType = query.queryType;
compositeQuery.panelType = panelType || PANEL_TYPES.TIME_SERIES;
compositeQuery.promQueries = promQueries;
return compositeQuery;
};
const queryTypeMethodMapping = {
[EQueryType.QUERY_BUILDER]: buildBuilderQuery,
[EQueryType.CLICKHOUSE]: buildClickHouseQuery,
[EQueryType.PROM]: buildPromQuery,
};
export const mapCompositeQueryFromQuery = (
query: Query,
panelType: PANEL_TYPES | null,
): ICompositeMetricQuery => {
const functionToBuildQuery = queryTypeMethodMapping[query.queryType];
if (functionToBuildQuery) {
return functionToBuildQuery(query, panelType);
}
return {
queryType: query.queryType,
panelType: panelType || PANEL_TYPES.TIME_SERIES,
builderQueries: {},
chQueries: {},
promQueries: {},
unit: undefined,
};
};

View File

@ -1,10 +1,10 @@
import { Col, Row } from 'antd';
import ExplorerCard from 'components/ExplorerCard';
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
import LogsExplorerViews from 'container/LogsExplorerViews';
import LogsTopNav from 'container/LogsTopNav';
import { DataSource } from 'types/common/queryBuilder';
// ** Styles
import { WrapperStyled } from './styles';
function LogsExplorer(): JSX.Element {
@ -14,7 +14,7 @@ function LogsExplorer(): JSX.Element {
<WrapperStyled>
<Row gutter={[0, 16]}>
<Col xs={24}>
<ExplorerCard>
<ExplorerCard sourcepage={DataSource.LOGS}>
<LogExplorerQuerySection />
</ExplorerCard>
</Col>

View File

@ -1,25 +1,20 @@
import { Tabs } from 'antd';
import axios from 'axios';
import ExplorerCard from 'components/ExplorerCard';
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
import {
initialAutocompleteData,
initialQueriesMap,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ExportPanel from 'container/ExportPanel';
import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/constants';
import QuerySection from 'container/TracesExplorer/QuerySection';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useCallback, useEffect, useMemo } from 'react';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
@ -33,10 +28,12 @@ function TracesExplorer(): JSX.Element {
currentQuery,
panelType,
updateAllQueriesOperators,
updateQueriesData,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const currentPanelType = useGetPanelTypesQueryParam();
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const currentTab = panelType || PANEL_TYPES.LIST;
const isMultipleQueries = useMemo(
@ -151,44 +148,6 @@ function TracesExplorer(): JSX.Element {
[exportDefaultQuery, notifications, panelType, updateDashboard],
);
const getUpdateQuery = useCallback(
(newPanelType: PANEL_TYPES): Query => {
let query = updateAllQueriesOperators(
currentQuery,
newPanelType,
DataSource.TRACES,
);
if (
newPanelType === PANEL_TYPES.LIST ||
newPanelType === PANEL_TYPES.TRACE
) {
query = updateQueriesData(query, 'queryData', (item) => ({
...item,
orderBy: item.orderBy.filter((item) => item.columnName !== SIGNOZ_VALUE),
aggregateAttribute: initialAutocompleteData,
}));
}
return query;
},
[currentQuery, updateAllQueriesOperators, updateQueriesData],
);
const handleTabChange = useCallback(
(type: string): void => {
const newPanelType = type as PANEL_TYPES;
if (panelType === newPanelType) return;
const query = getUpdateQuery(newPanelType);
redirectWithQueryBuilderData(query, {
[queryParamNamesMap.panelTypes]: newPanelType,
});
},
[getUpdateQuery, panelType, redirectWithQueryBuilderData],
);
useShareBuilderUrl(defaultQuery);
useEffect(() => {
@ -198,13 +157,19 @@ function TracesExplorer(): JSX.Element {
(currentTab === PANEL_TYPES.LIST || currentTab === PANEL_TYPES.TRACE) &&
shouldChangeView
) {
handleTabChange(PANEL_TYPES.TIME_SERIES);
handleExplorerTabChange(currentPanelType || PANEL_TYPES.TIME_SERIES);
}
}, [currentTab, isMultipleQueries, isGroupByExist, handleTabChange]);
}, [
currentTab,
isMultipleQueries,
isGroupByExist,
handleExplorerTabChange,
currentPanelType,
]);
return (
<>
<ExplorerCard>
<ExplorerCard sourcepage={DataSource.TRACES}>
<QuerySection />
</ExplorerCard>
@ -221,7 +186,7 @@ function TracesExplorer(): JSX.Element {
defaultActiveKey={currentTab}
activeKey={currentTab}
items={tabsItems}
onChange={handleTabChange}
onChange={handleExplorerTabChange}
/>
</Container>
</>

View File

@ -0,0 +1,21 @@
import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
function MockQueryClientProvider({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
export default MockQueryClientProvider;

View File

@ -0,0 +1,51 @@
import { DataSource } from 'types/common/queryBuilder';
import { ICompositeMetricQuery } from '../alerts/compositeQuery';
export interface ViewProps {
uuid: string;
name: string;
category: string;
createdAt: string;
createdBy: string;
updatedAt: string;
updatedBy: string;
sourcePage: DataSource;
tags: string[];
compositeQuery: ICompositeMetricQuery;
extraData: string;
}
export interface AllViewsProps {
status: string;
data: ViewProps[];
}
export interface SaveViewProps {
compositeQuery: ICompositeMetricQuery;
sourcePage: DataSource;
viewName: string;
extraData: string;
}
export interface SaveViewPayloadProps {
status: string;
data: string;
}
export interface DeleteViewPayloadProps {
status: string;
}
export interface UpdateViewProps {
viewKey: string;
compositeQuery: ICompositeMetricQuery;
extraData: string;
sourcePage: string;
viewName: string;
}
export interface UpdateViewPayloadProps {
success: string;
data: ViewProps;
}