mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 04:45:57 +08:00
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:
parent
15f328eb9e
commit
1138c6e41a
5
frontend/src/api/saveView/deleteView.ts
Normal file
5
frontend/src/api/saveView/deleteView.ts
Normal 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}`);
|
9
frontend/src/api/saveView/getAllViews.ts
Normal file
9
frontend/src/api/saveView/getAllViews.ts
Normal 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}`);
|
16
frontend/src/api/saveView/saveView.ts
Normal file
16
frontend/src/api/saveView/saveView.ts
Normal 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,
|
||||
});
|
19
frontend/src/api/saveView/updateView.ts
Normal file
19
frontend/src/api/saveView/updateView.ts
Normal 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,
|
||||
});
|
278
frontend/src/components/ExplorerCard/ExplorerCard.tsx
Normal file
278
frontend/src/components/ExplorerCard/ExplorerCard.tsx
Normal 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;
|
87
frontend/src/components/ExplorerCard/MenuItemGenerator.tsx
Normal file
87
frontend/src/components/ExplorerCard/MenuItemGenerator.tsx
Normal 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;
|
68
frontend/src/components/ExplorerCard/SaveViewWithName.tsx
Normal file
68
frontend/src/components/ExplorerCard/SaveViewWithName.tsx
Normal 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;
|
32
frontend/src/components/ExplorerCard/__mock__/viewData.ts
Normal file
32
frontend/src/components/ExplorerCard/__mock__/viewData.ts
Normal 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',
|
||||
},
|
||||
];
|
10
frontend/src/components/ExplorerCard/constants.ts
Normal file
10
frontend/src/components/ExplorerCard/constants.ts
Normal 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',
|
||||
};
|
@ -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;
|
28
frontend/src/components/ExplorerCard/styles.ts
Normal file
28
frontend/src/components/ExplorerCard/styles.ts
Normal 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;
|
||||
`;
|
@ -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();
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
77
frontend/src/components/ExplorerCard/types.ts
Normal file
77
frontend/src/components/ExplorerCard/types.ts
Normal 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;
|
||||
}
|
170
frontend/src/components/ExplorerCard/utils.ts
Normal file
170
frontend/src/components/ExplorerCard/utils.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
};
|
@ -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',
|
||||
};
|
||||
|
@ -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
|
||||
/>
|
||||
|
||||
|
15
frontend/src/hooks/queryBuilder/useGetSearchQueryParam.ts
Normal file
15
frontend/src/hooks/queryBuilder/useGetSearchQueryParam.ts
Normal 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]);
|
||||
};
|
11
frontend/src/hooks/saveViews/useDeleteView.ts
Normal file
11
frontend/src/hooks/saveViews/useDeleteView.ts
Normal 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),
|
||||
});
|
13
frontend/src/hooks/saveViews/useGetAllViews.ts
Normal file
13
frontend/src/hooks/saveViews/useGetAllViews.ts
Normal 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),
|
||||
});
|
26
frontend/src/hooks/saveViews/useSaveView.ts
Normal file
26
frontend/src/hooks/saveViews/useSaveView.ts
Normal 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,
|
||||
}),
|
||||
});
|
30
frontend/src/hooks/saveViews/useUpdateView.ts
Normal file
30
frontend/src/hooks/saveViews/useUpdateView.ts
Normal 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,
|
||||
}),
|
||||
});
|
81
frontend/src/hooks/useHandleExplorerTabChange.ts
Normal file
81
frontend/src/hooks/useHandleExplorerTabChange.ts
Normal 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;
|
||||
}
|
@ -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,
|
||||
};
|
||||
};
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
|
21
frontend/src/providers/test/MockQueryClientProvider.tsx
Normal file
21
frontend/src/providers/test/MockQueryClientProvider.tsx
Normal 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;
|
51
frontend/src/types/api/saveViews/types.ts
Normal file
51
frontend/src/types/api/saveViews/types.ts
Normal 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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user