mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-15 02:26:09 +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'
|
| 'selectedFields'
|
||||||
| 'linesPerRow';
|
| 'linesPerRow';
|
||||||
|
|
||||||
|
export type QuerySearchParamNames = 'viewName' | 'viewKey';
|
||||||
|
|
||||||
export const queryParamNamesMap: Record<QueryParamNames, QueryParamNames> = {
|
export const queryParamNamesMap: Record<QueryParamNames, QueryParamNames> = {
|
||||||
compositeQuery: 'compositeQuery',
|
compositeQuery: 'compositeQuery',
|
||||||
panelTypes: 'panelTypes',
|
panelTypes: 'panelTypes',
|
||||||
@ -14,3 +16,11 @@ export const queryParamNamesMap: Record<QueryParamNames, QueryParamNames> = {
|
|||||||
selectedFields: 'selectedFields',
|
selectedFields: 'selectedFields',
|
||||||
linesPerRow: 'linesPerRow',
|
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 TabLabel from 'components/TabLabel';
|
||||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||||
import {
|
import {
|
||||||
initialAutocompleteData,
|
|
||||||
initialFilters,
|
initialFilters,
|
||||||
initialQueriesMap,
|
initialQueriesMap,
|
||||||
initialQueryBuilderFormValues,
|
initialQueryBuilderFormValues,
|
||||||
@ -15,7 +14,6 @@ import GoToTop from 'container/GoToTop';
|
|||||||
import LogsExplorerChart from 'container/LogsExplorerChart';
|
import LogsExplorerChart from 'container/LogsExplorerChart';
|
||||||
import LogsExplorerList from 'container/LogsExplorerList';
|
import LogsExplorerList from 'container/LogsExplorerList';
|
||||||
import LogsExplorerTable from 'container/LogsExplorerTable';
|
import LogsExplorerTable from 'container/LogsExplorerTable';
|
||||||
import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/constants';
|
|
||||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
||||||
@ -24,6 +22,7 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
|||||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import useAxiosError from 'hooks/useAxiosError';
|
import useAxiosError from 'hooks/useAxiosError';
|
||||||
|
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
|
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
|
||||||
@ -67,10 +66,10 @@ function LogsExplorerViews(): JSX.Element {
|
|||||||
stagedQuery,
|
stagedQuery,
|
||||||
panelType,
|
panelType,
|
||||||
updateAllQueriesOperators,
|
updateAllQueriesOperators,
|
||||||
updateQueriesData,
|
|
||||||
redirectWithQueryBuilderData,
|
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
|
|
||||||
|
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const [page, setPage] = useState<number>(1);
|
const [page, setPage] = useState<number>(1);
|
||||||
const [logs, setLogs] = useState<ILog[]>([]);
|
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(
|
const getRequestData = useCallback(
|
||||||
(
|
(
|
||||||
query: Query | null,
|
query: Query | null,
|
||||||
@ -362,9 +325,9 @@ function LogsExplorerViews(): JSX.Element {
|
|||||||
const shouldChangeView = isMultipleQueries || isGroupByExist;
|
const shouldChangeView = isMultipleQueries || isGroupByExist;
|
||||||
|
|
||||||
if (panelType === PANEL_TYPES.LIST && shouldChangeView) {
|
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(() => {
|
useEffect(() => {
|
||||||
const currentParams = data?.params as Omit<LogTimeRange, 'pageSize'>;
|
const currentParams = data?.params as Omit<LogTimeRange, 'pageSize'>;
|
||||||
@ -518,7 +481,7 @@ function LogsExplorerViews(): JSX.Element {
|
|||||||
items={tabsItems}
|
items={tabsItems}
|
||||||
defaultActiveKey={panelType || PANEL_TYPES.LIST}
|
defaultActiveKey={panelType || PANEL_TYPES.LIST}
|
||||||
activeKey={panelType || PANEL_TYPES.LIST}
|
activeKey={panelType || PANEL_TYPES.LIST}
|
||||||
onChange={handleChangeView}
|
onChange={handleExplorerTabChange}
|
||||||
destroyInactiveTabPane
|
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 { Col, Row } from 'antd';
|
||||||
import ExplorerCard from 'components/ExplorerCard';
|
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||||
import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
|
import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
|
||||||
import LogsExplorerViews from 'container/LogsExplorerViews';
|
import LogsExplorerViews from 'container/LogsExplorerViews';
|
||||||
import LogsTopNav from 'container/LogsTopNav';
|
import LogsTopNav from 'container/LogsTopNav';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
// ** Styles
|
|
||||||
import { WrapperStyled } from './styles';
|
import { WrapperStyled } from './styles';
|
||||||
|
|
||||||
function LogsExplorer(): JSX.Element {
|
function LogsExplorer(): JSX.Element {
|
||||||
@ -14,7 +14,7 @@ function LogsExplorer(): JSX.Element {
|
|||||||
<WrapperStyled>
|
<WrapperStyled>
|
||||||
<Row gutter={[0, 16]}>
|
<Row gutter={[0, 16]}>
|
||||||
<Col xs={24}>
|
<Col xs={24}>
|
||||||
<ExplorerCard>
|
<ExplorerCard sourcepage={DataSource.LOGS}>
|
||||||
<LogExplorerQuerySection />
|
<LogExplorerQuerySection />
|
||||||
</ExplorerCard>
|
</ExplorerCard>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -1,25 +1,20 @@
|
|||||||
import { Tabs } from 'antd';
|
import { Tabs } from 'antd';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import ExplorerCard from 'components/ExplorerCard';
|
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||||
import {
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
initialAutocompleteData,
|
|
||||||
initialQueriesMap,
|
|
||||||
PANEL_TYPES,
|
|
||||||
} from 'constants/queryBuilder';
|
|
||||||
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
|
|
||||||
import ExportPanel from 'container/ExportPanel';
|
import ExportPanel from 'container/ExportPanel';
|
||||||
import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/constants';
|
|
||||||
import QuerySection from 'container/TracesExplorer/QuerySection';
|
import QuerySection from 'container/TracesExplorer/QuerySection';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
||||||
|
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
|
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||||
|
|
||||||
@ -33,10 +28,12 @@ function TracesExplorer(): JSX.Element {
|
|||||||
currentQuery,
|
currentQuery,
|
||||||
panelType,
|
panelType,
|
||||||
updateAllQueriesOperators,
|
updateAllQueriesOperators,
|
||||||
updateQueriesData,
|
|
||||||
redirectWithQueryBuilderData,
|
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
|
|
||||||
|
const currentPanelType = useGetPanelTypesQueryParam();
|
||||||
|
|
||||||
|
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||||
|
|
||||||
const currentTab = panelType || PANEL_TYPES.LIST;
|
const currentTab = panelType || PANEL_TYPES.LIST;
|
||||||
|
|
||||||
const isMultipleQueries = useMemo(
|
const isMultipleQueries = useMemo(
|
||||||
@ -151,44 +148,6 @@ function TracesExplorer(): JSX.Element {
|
|||||||
[exportDefaultQuery, notifications, panelType, updateDashboard],
|
[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);
|
useShareBuilderUrl(defaultQuery);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -198,13 +157,19 @@ function TracesExplorer(): JSX.Element {
|
|||||||
(currentTab === PANEL_TYPES.LIST || currentTab === PANEL_TYPES.TRACE) &&
|
(currentTab === PANEL_TYPES.LIST || currentTab === PANEL_TYPES.TRACE) &&
|
||||||
shouldChangeView
|
shouldChangeView
|
||||||
) {
|
) {
|
||||||
handleTabChange(PANEL_TYPES.TIME_SERIES);
|
handleExplorerTabChange(currentPanelType || PANEL_TYPES.TIME_SERIES);
|
||||||
}
|
}
|
||||||
}, [currentTab, isMultipleQueries, isGroupByExist, handleTabChange]);
|
}, [
|
||||||
|
currentTab,
|
||||||
|
isMultipleQueries,
|
||||||
|
isGroupByExist,
|
||||||
|
handleExplorerTabChange,
|
||||||
|
currentPanelType,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ExplorerCard>
|
<ExplorerCard sourcepage={DataSource.TRACES}>
|
||||||
<QuerySection />
|
<QuerySection />
|
||||||
</ExplorerCard>
|
</ExplorerCard>
|
||||||
|
|
||||||
@ -221,7 +186,7 @@ function TracesExplorer(): JSX.Element {
|
|||||||
defaultActiveKey={currentTab}
|
defaultActiveKey={currentTab}
|
||||||
activeKey={currentTab}
|
activeKey={currentTab}
|
||||||
items={tabsItems}
|
items={tabsItems}
|
||||||
onChange={handleTabChange}
|
onChange={handleExplorerTabChange}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</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