diff --git a/frontend/src/api/saveView/deleteView.ts b/frontend/src/api/saveView/deleteView.ts new file mode 100644 index 0000000000..e58e731d10 --- /dev/null +++ b/frontend/src/api/saveView/deleteView.ts @@ -0,0 +1,5 @@ +import axios from 'api'; +import { DeleteViewPayloadProps } from 'types/api/saveViews/types'; + +export const deleteView = (uuid: string): Promise => + axios.delete(`explorer/views/${uuid}`); diff --git a/frontend/src/api/saveView/getAllViews.ts b/frontend/src/api/saveView/getAllViews.ts new file mode 100644 index 0000000000..bdafb96b61 --- /dev/null +++ b/frontend/src/api/saveView/getAllViews.ts @@ -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> => + axios.get(`explorer/views?sourcePage=${sourcepage}`); diff --git a/frontend/src/api/saveView/saveView.ts b/frontend/src/api/saveView/saveView.ts new file mode 100644 index 0000000000..a0c7ba5bf4 --- /dev/null +++ b/frontend/src/api/saveView/saveView.ts @@ -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> => + axios.post('explorer/views', { + name: viewName, + sourcePage, + compositeQuery, + extraData, + }); diff --git a/frontend/src/api/saveView/updateView.ts b/frontend/src/api/saveView/updateView.ts new file mode 100644 index 0000000000..6ee745ffc2 --- /dev/null +++ b/frontend/src/api/saveView/updateView.ts @@ -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 => + axios.put(`explorer/views/${viewKey}`, { + name: viewName, + compositeQuery, + extraData, + sourcePage, + }); diff --git a/frontend/src/components/ExplorerCard/ExplorerCard.tsx b/frontend/src/components/ExplorerCard/ExplorerCard.tsx new file mode 100644 index 0000000000..a7dda9ce95 --- /dev/null +++ b/frontend/src/components/ExplorerCard/ExplorerCard.tsx @@ -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(false); + const [, setCopyUrl] = useCopyToClipboard(); + const [isQueryUpdated, setIsQueryUpdated] = useState(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: ( + + ), + })), + }), + [refetchAllView, viewKey, viewsData?.data?.data], + ); + + const moreOptionMenu = useMemo( + (): MenuProps => ({ + items: [ + { + key: 'delete', + label: Delete, + onClick: onDeleteHandler, + icon: , + }, + ], + }), + [onDeleteHandler], + ); + + const saveButtonType = isQueryUpdated ? 'default' : 'primary'; + const saveButtonIcon = isQueryUpdated ? null : ; + + return ( + <> + + + + + {viewName} + + + + + + {viewsData?.data.data && viewsData?.data.data.length && ( + + {/* Saved Views */} + } + trigger={['click']} + overlayStyle={DropDownOverlay} + > + Select View + + + )} + {isQueryUpdated && ( + + )} + + } + showArrow={false} + open={isOpen} + onOpenChange={handleOpenChange} + > + + + + {viewKey && ( + + + + )} + + + + + {children} + + ); +} + +export default ExplorerCard; diff --git a/frontend/src/components/ExplorerCard/MenuItemGenerator.tsx b/frontend/src/components/ExplorerCard/MenuItemGenerator.tsx new file mode 100644 index 0000000000..2b101385e9 --- /dev/null +++ b/frontend/src/components/ExplorerCard/MenuItemGenerator.tsx @@ -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): 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 ( + + + + + {viewName} + + + Created by {createdBy} + + + + + + + + + + ); +} + +export default MenuItemGenerator; diff --git a/frontend/src/components/ExplorerCard/SaveViewWithName.tsx b/frontend/src/components/ExplorerCard/SaveViewWithName.tsx new file mode 100644 index 0000000000..3f77a769c8 --- /dev/null +++ b/frontend/src/components/ExplorerCard/SaveViewWithName.tsx @@ -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): 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 ( + + Name of the View + + + Save + + + ); +} + +export default SaveViewWithName; diff --git a/frontend/src/components/ExplorerCard/__mock__/viewData.ts b/frontend/src/components/ExplorerCard/__mock__/viewData.ts new file mode 100644 index 0000000000..5423cc98dc --- /dev/null +++ b/frontend/src/components/ExplorerCard/__mock__/viewData.ts @@ -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', + }, +]; diff --git a/frontend/src/components/ExplorerCard/constants.ts b/frontend/src/components/ExplorerCard/constants.ts new file mode 100644 index 0000000000..8caffb366c --- /dev/null +++ b/frontend/src/components/ExplorerCard/constants.ts @@ -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', +}; diff --git a/frontend/src/components/ExplorerCard/index.tsx b/frontend/src/components/ExplorerCard/index.tsx deleted file mode 100644 index fd8f3ceb81..0000000000 --- a/frontend/src/components/ExplorerCard/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Card, Space, Typography } from 'antd'; -import TextToolTip from 'components/TextToolTip'; - -function ExplorerCard({ children }: Props): JSX.Element { - return ( - - Query Builder - - - } - > - {children} - - ); -} - -interface Props { - children: React.ReactNode; -} - -export default ExplorerCard; diff --git a/frontend/src/components/ExplorerCard/styles.ts b/frontend/src/components/ExplorerCard/styles.ts new file mode 100644 index 0000000000..63ed068c53 --- /dev/null +++ b/frontend/src/components/ExplorerCard/styles.ts @@ -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; +`; diff --git a/frontend/src/components/ExplorerCard/test/ExplorerCard.test.tsx b/frontend/src/components/ExplorerCard/test/ExplorerCard.test.tsx new file mode 100644 index 0000000000..c7289756d7 --- /dev/null +++ b/frontend/src/components/ExplorerCard/test/ExplorerCard.test.tsx @@ -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( + + child + , + ); + expect(screen.getByText('Query Builder')).toBeInTheDocument(); + }); + + it('renders a save view button', () => { + render( + + child + , + ); + expect(screen.getByText('Save view')).toBeInTheDocument(); + }); + + it('should see all the view listed in dropdown', async () => { + const screen = render( + Mock Children, + ); + 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(); + }); +}); diff --git a/frontend/src/components/ExplorerCard/test/MenuItemGenerator.test.tsx b/frontend/src/components/ExplorerCard/test/MenuItemGenerator.test.tsx new file mode 100644 index 0000000000..de4f9c06a7 --- /dev/null +++ b/frontend/src/components/ExplorerCard/test/MenuItemGenerator.test.tsx @@ -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( + + + , + ); + + expect(screen.getByText(viewMockData[0].name)).toBeInTheDocument(); + }); + + it('should call onMenuItemSelectHandler on click of MenuItemGenerator', () => { + render( + + + , + ); + + const spanElement = screen.getByRole('img', { + name: 'delete', + }); + + expect(spanElement).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/ExplorerCard/test/SaveViewWithName.test.tsx b/frontend/src/components/ExplorerCard/test/SaveViewWithName.test.tsx new file mode 100644 index 0000000000..8e6664ac8c --- /dev/null +++ b/frontend/src/components/ExplorerCard/test/SaveViewWithName.test.tsx @@ -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( + + + , + ); + + expect(screen.getByText('Save')).toBeInTheDocument(); + }); + + it('should call saveViewAsync on click of Save button', () => { + const screen = render( + + + , + ); + + fireEvent.click(screen.getByText('Save')); + + expect(screen.getByText('Save')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/ExplorerCard/types.ts b/frontend/src/components/ExplorerCard/types.ts new file mode 100644 index 0000000000..bda7f702b9 --- /dev/null +++ b/frontend/src/components/ExplorerCard/types.ts @@ -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, + Error, + SaveViewProps, + SaveViewPayloadProps + >; + handlePopOverClose: SaveViewWithNameProps['handlePopOverClose']; + redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData']; + setName: (value: SetStateAction) => void; +} + +export interface DeleteViewHandlerProps { + deleteViewAsync: UseMutateAsyncFunction; + refetchAllView: MenuItemLabelGeneratorProps['refetchAllView']; + redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData']; + notifications: NotificationInstance; + panelType: PANEL_TYPES | null; + viewKey: string; + viewId: string; +} diff --git a/frontend/src/components/ExplorerCard/utils.ts b/frontend/src/components/ExplorerCard/utils.ts new file mode 100644 index 0000000000..e846eb4c6a --- /dev/null +++ b/frontend/src/components/ExplorerCard/utils.ts @@ -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); + }, + }); +}; diff --git a/frontend/src/constants/queryBuilderQueryNames.ts b/frontend/src/constants/queryBuilderQueryNames.ts index b3ee34cf89..fd7311364b 100644 --- a/frontend/src/constants/queryBuilderQueryNames.ts +++ b/frontend/src/constants/queryBuilderQueryNames.ts @@ -6,6 +6,8 @@ type QueryParamNames = | 'selectedFields' | 'linesPerRow'; +export type QuerySearchParamNames = 'viewName' | 'viewKey'; + export const queryParamNamesMap: Record = { compositeQuery: 'compositeQuery', panelTypes: 'panelTypes', @@ -14,3 +16,11 @@ export const queryParamNamesMap: Record = { selectedFields: 'selectedFields', linesPerRow: 'linesPerRow', }; + +export const querySearchParams: Record< + QuerySearchParamNames, + QuerySearchParamNames +> = { + viewName: 'viewName', + viewKey: 'viewKey', +}; diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index cab522c156..9331d089d4 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -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(1); const [logs, setLogs] = useState([]); @@ -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; @@ -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 /> diff --git a/frontend/src/hooks/queryBuilder/useGetSearchQueryParam.ts b/frontend/src/hooks/queryBuilder/useGetSearchQueryParam.ts new file mode 100644 index 0000000000..aa4185e900 --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useGetSearchQueryParam.ts @@ -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]); +}; diff --git a/frontend/src/hooks/saveViews/useDeleteView.ts b/frontend/src/hooks/saveViews/useDeleteView.ts new file mode 100644 index 0000000000..a867714d39 --- /dev/null +++ b/frontend/src/hooks/saveViews/useDeleteView.ts @@ -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 => + useMutation({ + mutationKey: [uuid], + mutationFn: () => deleteView(uuid), + }); diff --git a/frontend/src/hooks/saveViews/useGetAllViews.ts b/frontend/src/hooks/saveViews/useGetAllViews.ts new file mode 100644 index 0000000000..cb0ce6ba9d --- /dev/null +++ b/frontend/src/hooks/saveViews/useGetAllViews.ts @@ -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, AxiosError> => + useQuery, AxiosError>({ + queryKey: [{ sourcepage }], + queryFn: () => getAllViews(sourcepage), + }); diff --git a/frontend/src/hooks/saveViews/useSaveView.ts b/frontend/src/hooks/saveViews/useSaveView.ts new file mode 100644 index 0000000000..d3f4da00ef --- /dev/null +++ b/frontend/src/hooks/saveViews/useSaveView.ts @@ -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, + Error, + SaveViewProps, + SaveViewPayloadProps +> => + useMutation({ + mutationKey: [viewName, sourcePage, compositeQuery, extraData], + mutationFn: () => + saveView({ + compositeQuery, + sourcePage, + viewName, + extraData, + }), + }); diff --git a/frontend/src/hooks/saveViews/useUpdateView.ts b/frontend/src/hooks/saveViews/useUpdateView.ts new file mode 100644 index 0000000000..bc6049e3fa --- /dev/null +++ b/frontend/src/hooks/saveViews/useUpdateView.ts @@ -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, + }), + }); diff --git a/frontend/src/hooks/useHandleExplorerTabChange.ts b/frontend/src/hooks/useHandleExplorerTabChange.ts new file mode 100644 index 0000000000..a4b605362c --- /dev/null +++ b/frontend/src/hooks/useHandleExplorerTabChange.ts @@ -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; +} diff --git a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery.ts b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery.ts new file mode 100644 index 0000000000..569f714543 --- /dev/null +++ b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery.ts @@ -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, + }; +}; diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx index 85514dfa5b..3aa0f95d1b 100644 --- a/frontend/src/pages/LogsExplorer/index.tsx +++ b/frontend/src/pages/LogsExplorer/index.tsx @@ -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 { - + diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx index 6a89825fd1..c83ade514a 100644 --- a/frontend/src/pages/TracesExplorer/index.tsx +++ b/frontend/src/pages/TracesExplorer/index.tsx @@ -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 ( <> - + @@ -221,7 +186,7 @@ function TracesExplorer(): JSX.Element { defaultActiveKey={currentTab} activeKey={currentTab} items={tabsItems} - onChange={handleTabChange} + onChange={handleExplorerTabChange} /> diff --git a/frontend/src/providers/test/MockQueryClientProvider.tsx b/frontend/src/providers/test/MockQueryClientProvider.tsx new file mode 100644 index 0000000000..e76f67ec00 --- /dev/null +++ b/frontend/src/providers/test/MockQueryClientProvider.tsx @@ -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 ( + {children} + ); +} + +export default MockQueryClientProvider; diff --git a/frontend/src/types/api/saveViews/types.ts b/frontend/src/types/api/saveViews/types.ts new file mode 100644 index 0000000000..b0be0110a6 --- /dev/null +++ b/frontend/src/types/api/saveViews/types.ts @@ -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; +}