Enhancement for Save View PRD. (#3471)

* refactor: search in dropdown

* refactor: name of the view to i18

* refactor: make the use of useForm from antd

* refactor: moved QuerySearchParamNames into save view module

* refactor: reset to query build when click on explorer link

* refactor: reverted resetQuery in querybuilder

---------

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
Rajat Dabade 2023-09-08 20:36:21 +05:30 committed by GitHub
parent f17608fa10
commit 004f10e73b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 164 additions and 128 deletions

View File

@ -0,0 +1,3 @@
{
"name_of_the_view": "Name of the view"
}

View File

@ -0,0 +1,3 @@
{
"name_of_the_view": "Name of the view"
}

View File

@ -1,6 +1,5 @@
import {
DeleteOutlined,
DownOutlined,
MoreOutlined,
SaveOutlined,
ShareAltOutlined,
@ -13,13 +12,13 @@ import {
MenuProps,
Popover,
Row,
Select,
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';
@ -28,10 +27,14 @@ 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 { useEffect, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { ExploreHeaderToolTip, SaveButtonText } from './constants';
import {
ExploreHeaderToolTip,
querySearchParams,
SaveButtonText,
} from './constants';
import MenuItemGenerator from './MenuItemGenerator';
import SaveViewWithName from './SaveViewWithName';
import {
@ -63,6 +66,7 @@ function ExplorerCard({
currentQuery,
panelType,
redirectWithQueryBuilderData,
updateAllQueriesOperators,
} = useQueryBuilder();
const {
@ -75,11 +79,7 @@ function ExplorerCard({
useErrorNotification(error);
const handlePopOverClose = (): void => {
setIsOpen(false);
};
const handleOpenChange = (newOpen: boolean): void => {
const handleOpenChange = (newOpen = false): void => {
setIsOpen(newOpen);
};
@ -104,7 +104,7 @@ function ExplorerCard({
});
};
const onDeleteHandler = useCallback(() => {
const onDeleteHandler = (): void =>
deleteViewHandler({
deleteViewAsync,
notifications,
@ -113,15 +113,9 @@ function ExplorerCard({
refetchAllView,
viewId: viewKey,
viewKey,
updateAllQueriesOperators,
sourcePage: sourcepage,
});
}, [
deleteViewAsync,
notifications,
panelType,
redirectWithQueryBuilderData,
refetchAllView,
viewKey,
]);
const onUpdateQueryHandler = (): void => {
updateViewAsync(
@ -165,38 +159,16 @@ function ExplorerCard({
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 moreOptionMenu: MenuProps = {
items: [
{
key: 'delete',
label: <Typography.Text strong>Delete</Typography.Text>,
onClick: onDeleteHandler,
icon: <DeleteOutlined />,
},
],
};
const saveButtonType = isQueryUpdated ? 'default' : 'primary';
const saveButtonIcon = isQueryUpdated ? null : <SaveOutlined />;
@ -215,20 +187,32 @@ function ExplorerCard({
/>
</Space>
</Col>
<OffSetCol span={10} offset={8}>
<OffSetCol span={18}>
<Space size="large">
{viewsData?.data.data && viewsData?.data.data.length && (
<Space>
{/* <Typography.Text>Saved Views</Typography.Text> */}
<Dropdown.Button
menu={menu}
<Select
loading={isLoading || isRefetching}
icon={<DownOutlined />}
trigger={['click']}
overlayStyle={DropDownOverlay}
showSearch
placeholder="Select a view"
dropdownStyle={DropDownOverlay}
dropdownMatchSelectWidth={false}
value={null}
>
Select View
</Dropdown.Button>
{viewsData?.data.data.map((view) => (
<Select.Option key={view.uuid} value={view.name}>
<MenuItemGenerator
viewName={view.name}
viewKey={viewKey}
createdBy={view.createdBy}
uuid={view.uuid}
refetchAllView={refetchAllView}
viewData={viewsData.data.data}
sourcePage={sourcepage}
/>
</Select.Option>
))}
</Select>
</Space>
)}
{isQueryUpdated && (
@ -246,7 +230,7 @@ function ExplorerCard({
content={
<SaveViewWithName
sourcePage={sourcepage}
handlePopOverClose={handlePopOverClose}
handlePopOverClose={handleOpenChange}
refetchAllView={refetchAllView}
/>
}

View File

@ -17,9 +17,15 @@ function MenuItemGenerator({
uuid,
viewData,
refetchAllView,
sourcePage,
}: MenuItemLabelGeneratorProps): JSX.Element {
const { panelType, redirectWithQueryBuilderData } = useQueryBuilder();
const {
panelType,
redirectWithQueryBuilderData,
updateAllQueriesOperators,
} = useQueryBuilder();
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const { notifications } = useNotifications();
const { mutateAsync: deleteViewAsync } = useDeleteView(uuid);
@ -34,6 +40,8 @@ function MenuItemGenerator({
refetchAllView,
viewId: uuid,
viewKey,
updateAllQueriesOperators,
sourcePage,
});
};

View File

@ -1,13 +1,13 @@
import { Card, Input, Typography } from 'antd';
import { Card, Form, 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 { useTranslation } from 'react-i18next';
import { SaveButton } from './styles';
import { SaveViewWithNameProps } from './types';
import { SaveViewFormProps, SaveViewWithNameProps } from './types';
import { saveViewHandler } from './utils';
function SaveViewWithName({
@ -15,7 +15,8 @@ function SaveViewWithName({
handlePopOverClose,
refetchAllView,
}: SaveViewWithNameProps): JSX.Element {
const [name, setName] = useState('');
const [form] = Form.useForm<SaveViewFormProps>();
const { t } = useTranslation(['explorer']);
const {
currentQuery,
panelType,
@ -25,19 +26,12 @@ function SaveViewWithName({
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
const { isLoading, mutateAsync: saveViewAsync } = useSaveView({
viewName: name,
viewName: form.getFieldValue('viewName'),
compositeQuery,
sourcePage,
extraData: '',
});
const onChangeHandler = useCallback(
(e: ChangeEvent<HTMLInputElement>): void => {
setName(e.target.value);
},
[],
);
const onSaveHandler = (): void => {
saveViewHandler({
compositeQuery,
@ -49,18 +43,32 @@ function SaveViewWithName({
refetchAllView,
saveViewAsync,
sourcePage,
viewName: name,
setName,
viewName: form.getFieldValue('viewName'),
form,
});
};
return (
<Card>
<Typography>Name of the View</Typography>
<Input placeholder="Enter Name" onChange={onChangeHandler} />
<SaveButton onClick={onSaveHandler} type="primary" loading={isLoading}>
Save
</SaveButton>
<Typography>{t('name_of_the_view')}</Typography>
<Form form={form} onFinish={onSaveHandler}>
<Form.Item
name={['viewName']}
required
requiredMark
rules={[
{
required: true,
message: 'Please enter view name',
},
]}
>
<Input placeholder="Enter Name" />
</Form.Item>
<SaveButton htmlType="submit" type="primary" loading={isLoading}>
Save
</SaveButton>
</Form>
</Card>
);
}

View File

@ -8,3 +8,13 @@ export const SaveButtonText = {
SAVE_AS_NEW_VIEW: 'Save as new view',
SAVE_VIEW: 'Save view',
};
export type QuerySearchParamNames = 'viewName' | 'viewKey';
export const querySearchParams: Record<
QuerySearchParamNames,
QuerySearchParamNames
> = {
viewName: 'viewName',
viewKey: 'viewKey',
};

View File

@ -62,15 +62,12 @@ describe('ExplorerCard', () => {
const screen = render(
<ExplorerCard sourcepage={DataSource.TRACES}>Mock Children</ExplorerCard>,
);
const selectButton = screen.getByText('Select View');
const selectPlaceholder = screen.getByText('Select a view');
fireEvent.click(selectButton);
const spanElement = screen.getByRole('img', {
name: 'down',
fireEvent.mouseDown(selectPlaceholder);
const viewNameText = await screen.getAllByText('View 1');
viewNameText.forEach((element) => {
expect(element).toBeInTheDocument();
});
fireEvent.click(spanElement);
const viewNameText = await screen.findByText('View 2');
expect(viewNameText).toBeInTheDocument();
});
});

View File

@ -1,6 +1,7 @@
import { 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 MenuItemGenerator from '../MenuItemGenerator';
@ -12,6 +13,12 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('antd/es/form/Form', () => ({
useForm: jest.fn().mockReturnValue({
onFinish: jest.fn(),
}),
}));
describe('MenuItemGenerator', () => {
it('should render MenuItemGenerator component', () => {
const screen = render(
@ -23,6 +30,7 @@ describe('MenuItemGenerator', () => {
uuid={viewMockData[0].uuid}
refetchAllView={jest.fn()}
viewData={viewMockData}
sourcePage={DataSource.TRACES}
/>
</MockQueryClientProvider>,
);
@ -40,6 +48,7 @@ describe('MenuItemGenerator', () => {
uuid={viewMockData[0].uuid}
refetchAllView={jest.fn()}
viewData={viewMockData}
sourcePage={DataSource.TRACES}
/>
</MockQueryClientProvider>,
);

View File

@ -1,7 +1,7 @@
import { FormInstance } from 'antd';
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';
@ -38,6 +38,10 @@ export interface SaveViewWithNameProps {
refetchAllView: VoidFunction;
}
export interface SaveViewFormProps {
viewName: string;
}
export interface MenuItemLabelGeneratorProps {
viewName: string;
viewKey: string;
@ -45,6 +49,7 @@ export interface MenuItemLabelGeneratorProps {
uuid: string;
viewData: ViewProps[];
refetchAllView: VoidFunction;
sourcePage: ExplorerCardProps['sourcepage'];
}
export interface SaveViewHandlerProps {
@ -63,7 +68,7 @@ export interface SaveViewHandlerProps {
>;
handlePopOverClose: SaveViewWithNameProps['handlePopOverClose'];
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
setName: (value: SetStateAction<string>) => void;
form: FormInstance<SaveViewFormProps>;
}
export interface DeleteViewHandlerProps {
@ -74,4 +79,6 @@ export interface DeleteViewHandlerProps {
panelType: PANEL_TYPES | null;
viewKey: string;
viewId: string;
updateAllQueriesOperators: QueryBuilderContextType['updateAllQueriesOperators'];
sourcePage: ExplorerCardProps['sourcepage'];
}

View File

@ -1,14 +1,12 @@
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 { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import isEqual from 'lodash-es/isEqual';
import { querySearchParams } from './constants';
import {
DeleteViewHandlerProps,
GetViewDetailsUsingViewKey,
@ -108,7 +106,7 @@ export const saveViewHandler = ({
extraData,
redirectWithQueryBuilderData,
panelType,
setName,
form,
}: SaveViewHandlerProps): void => {
saveViewAsync(
{
@ -134,7 +132,7 @@ export const saveViewHandler = ({
},
onSettled: () => {
handlePopOverClose();
setName('');
form.resetFields();
},
},
);
@ -148,15 +146,24 @@ export const deleteViewHandler = ({
panelType,
viewKey,
viewId,
updateAllQueriesOperators,
sourcePage,
}: DeleteViewHandlerProps): void => {
deleteViewAsync(viewKey, {
onSuccess: () => {
if (viewId === viewKey) {
redirectWithQueryBuilderData(initialQueriesMap.traces, {
[querySearchParams.viewName]: 'Query Builder',
[queryParamNamesMap.panelTypes]: panelType,
[querySearchParams.viewKey]: '',
});
redirectWithQueryBuilderData(
updateAllQueriesOperators(
initialQueriesMap.traces,
panelType || PANEL_TYPES.LIST,
sourcePage,
),
{
[querySearchParams.viewName]: 'Query Builder',
[queryParamNamesMap.panelTypes]: panelType,
[querySearchParams.viewKey]: '',
},
);
}
notifications.success({
message: 'View Deleted Successfully',

View File

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

View File

@ -1,4 +1,4 @@
import { QuerySearchParamNames } from 'constants/queryBuilderQueryNames';
import { QuerySearchParamNames } from 'components/ExplorerCard/constants';
import useUrlQuery from 'hooks/useUrlQuery';
import { useMemo } from 'react';

View File

@ -8,14 +8,21 @@ import { useQueryBuilder } from './useQueryBuilder';
export type UseShareBuilderUrlParams = { defaultValue: Query };
export const useShareBuilderUrl = (defaultQuery: Query): void => {
const { redirectWithQueryBuilderData } = useQueryBuilder();
const { resetQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const urlQuery = useUrlQuery();
const compositeQuery = useGetCompositeQueryParam();
useEffect(() => {
if (!compositeQuery) {
resetQuery(defaultQuery);
redirectWithQueryBuilderData(defaultQuery);
}
}, [defaultQuery, urlQuery, compositeQuery, redirectWithQueryBuilderData]);
}, [
defaultQuery,
urlQuery,
redirectWithQueryBuilderData,
compositeQuery,
resetQuery,
]);
};

View File

@ -16,11 +16,5 @@ export const useSaveView = ({
> =>
useMutation({
mutationKey: [viewName, sourcePage, compositeQuery, extraData],
mutationFn: () =>
saveView({
compositeQuery,
sourcePage,
viewName,
extraData,
}),
mutationFn: saveView,
});

View File

@ -1,8 +1,6 @@
import { querySearchParams } from 'components/ExplorerCard/constants';
import { initialAutocompleteData, PANEL_TYPES } from 'constants/queryBuilder';
import {
queryParamNamesMap,
querySearchParams,
} from 'constants/queryBuilderQueryNames';
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/constants';
import { useCallback } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@ -25,8 +23,7 @@ export const useHandleExplorerTabChange = (): {
updateQueriesData,
} = useQueryBuilder();
const viewName =
useGetSearchQueryParam(querySearchParams.viewName) || 'Query Builder';
const viewName = useGetSearchQueryParam(querySearchParams.viewName) || '';
const viewKey = useGetSearchQueryParam(querySearchParams.viewKey) || '';

View File

@ -68,6 +68,7 @@ export const QueryBuilderContext = createContext<QueryBuilderContextType>({
addNewQueryItem: () => {},
redirectWithQueryBuilderData: () => {},
handleRunQuery: () => {},
resetQuery: () => {},
updateAllQueriesOperators: () => initialQueriesMap.metrics,
updateQueriesData: () => initialQueriesMap.metrics,
initQueryBuilderData: () => {},
@ -550,6 +551,14 @@ export function QueryBuilderProvider({
stagedQuery,
]);
const resetQuery = (newCurrentQuery?: QueryState): void => {
setStagedQuery(null);
if (newCurrentQuery) {
setCurrentQuery(newCurrentQuery);
}
};
useEffect(() => {
if (stagedQuery && location.pathname !== currentPathnameRef.current) {
currentPathnameRef.current = location.pathname;
@ -599,6 +608,7 @@ export function QueryBuilderProvider({
addNewQueryItem,
redirectWithQueryBuilderData,
handleRunQuery,
resetQuery,
updateAllQueriesOperators,
updateQueriesData,
initQueryBuilderData,

View File

@ -6,6 +6,7 @@ import {
IClickHouseQuery,
IPromQLQuery,
Query,
QueryState,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from './dashboard';
@ -187,6 +188,7 @@ export type QueryBuilderContextType = {
searchParams?: Record<string, unknown>,
) => void;
handleRunQuery: () => void;
resetQuery: (newCurrentQuery?: QueryState) => void;
handleOnUnitsChange: (units: Format['id']) => void;
updateAllQueriesOperators: (
queryData: Query,