import './SaveView.styles.scss'; import { Color } from '@signozhq/design-tokens'; import { Button, ColorPicker, Input, Modal, Table, TableProps, Typography, } from 'antd'; import logEvent from 'api/common/logEvent'; import { getViewDetailsUsingViewKey, showErrorNotification, } from 'components/ExplorerCard/utils'; import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; import { getRandomColor } from 'container/ExplorerOptions/utils'; import { MeterExplorerEventKeys, MeterExplorerEvents, } from 'container/MeterExplorer/events'; import { MetricsExplorerEventKeys, MetricsExplorerEvents, } from 'container/MetricsExplorer/events'; import { useDeleteView } from 'hooks/saveViews/useDeleteView'; import { useGetAllViews } from 'hooks/saveViews/useGetAllViews'; import { useUpdateView } from 'hooks/saveViews/useUpdateView'; import useErrorNotification from 'hooks/useErrorNotification'; import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange'; import { useNotifications } from 'hooks/useNotifications'; import { CalendarClock, Check, Compass, PenLine, Search, Trash2, X, } from 'lucide-react'; import { useAppContext } from 'providers/App/App'; import { useTimezone } from 'providers/Timezone'; import { ChangeEvent, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery'; import { ViewProps } from 'types/api/saveViews/types'; import { DataSource } from 'types/common/queryBuilder'; import { USER_ROLES } from 'types/roles'; import { ROUTES_VS_SOURCEPAGE, SOURCEPAGE_VS_ROUTES } from './constants'; import { deleteViewHandler } from './utils'; const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR]; function SaveView(): JSX.Element { const { pathname } = useLocation(); const sourcepage = ROUTES_VS_SOURCEPAGE[pathname]; const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [activeViewKey, setActiveViewKey] = useState(''); const [newViewName, setNewViewName] = useState(''); const [color, setColor] = useState(Color.BG_SIENNA_500); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [activeViewName, setActiveViewName] = useState(''); const [ activeCompositeQuery, setActiveCompositeQuery, ] = useState(null); const [searchValue, setSearchValue] = useState(''); const [dataSource, setDataSource] = useState([]); const { t } = useTranslation(['explorer']); const hideDeleteViewModal = (): void => { setIsDeleteModalOpen(false); }; const { user } = useAppContext(); const handleDeleteModelOpen = (uuid: string, name: string): void => { setActiveViewKey(uuid); setActiveViewName(name); setIsDeleteModalOpen(true); }; const hideEditViewModal = (): void => { setIsEditModalOpen(false); }; const handleEditModelOpen = (view: ViewProps, color: string): void => { setActiveViewKey(view.id); setColor(color); setActiveViewName(view.name); setNewViewName(view.name); setActiveCompositeQuery(view.compositeQuery); setIsEditModalOpen(true); }; const { notifications } = useNotifications(); const { data: viewsData, isLoading, error, isRefetching, refetch: refetchAllView, } = useGetAllViews(sourcepage as DataSource); useEffect(() => { setDataSource(viewsData?.data.data || []); }, [viewsData?.data.data]); useErrorNotification(error); const handleSearch = (e: ChangeEvent): void => { setSearchValue(e.target.value); const filteredData = viewsData?.data.data.filter((view) => view.name.toLowerCase().includes(e.target.value.toLowerCase()), ); setDataSource(filteredData || []); }; const clearSearch = (): void => { setSearchValue(''); }; const { mutateAsync: deleteViewAsync, isLoading: isDeleteLoading, } = useDeleteView(activeViewKey); const onDeleteHandler = (): void => { deleteViewHandler({ deleteViewAsync, notifications, refetchAllView, viewId: activeViewKey, hideDeleteViewModal, clearSearch, }); }; const { mutateAsync: updateViewAsync, isLoading: isViewUpdating, } = useUpdateView({ compositeQuery: activeCompositeQuery || ({} as ICompositeMetricQuery), viewKey: activeViewKey, extraData: JSON.stringify({ color }), sourcePage: sourcepage || DataSource.LOGS, viewName: newViewName, }); const logEventCalledRef = useRef(false); useEffect(() => { if (!logEventCalledRef.current && !isLoading) { if (sourcepage === DataSource.TRACES) { logEvent('Traces Views: Views visited', { number: viewsData?.data?.data?.length, }); } else if (sourcepage === DataSource.LOGS) { logEvent('Logs Views: Views visited', { number: viewsData?.data?.data?.length, }); } else if (sourcepage === DataSource.METRICS) { logEvent(MetricsExplorerEvents.TabChanged, { [MetricsExplorerEventKeys.Tab]: 'views', }); } else if (sourcepage === 'meter') { logEvent(MeterExplorerEvents.TabChanged, { [MeterExplorerEventKeys.Tab]: 'views', }); } logEventCalledRef.current = true; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [viewsData?.data.data, isLoading]); const onUpdateQueryHandler = (): void => { updateViewAsync( { compositeQuery: activeCompositeQuery || ({} as ICompositeMetricQuery), viewKey: activeViewKey, extraData: JSON.stringify({ color }), sourcePage: sourcepage, viewName: activeViewName, }, { onSuccess: () => { notifications.success({ message: 'View Updated Successfully', }); hideEditViewModal(); refetchAllView(); logEvent(MetricsExplorerEvents.ViewEdited, { [MetricsExplorerEventKeys.Tab]: 'views', }); }, onError: (err) => { showErrorNotification(notifications, err); }, }, ); }; const { handleExplorerTabChange } = useHandleExplorerTabChange(); const handleRedirectQuery = (view: ViewProps): void => { const currentViewDetails = getViewDetailsUsingViewKey( view.id, viewsData?.data.data, ); if (!currentViewDetails) return; const { query, name, id, panelType: currentPanelType } = currentViewDetails; if (sourcepage) { handleExplorerTabChange( currentPanelType, { query, name, id, }, SOURCEPAGE_VS_ROUTES[sourcepage], ); logEvent(MetricsExplorerEvents.OpenInExplorerClicked, { [MetricsExplorerEventKeys.Tab]: 'views', [MetricsExplorerEventKeys.ViewName]: name, }); } }; const { formatTimezoneAdjustedTimestamp } = useTimezone(); const columns: TableProps['columns'] = [ { title: 'Save View', key: 'view', render: (view: ViewProps): JSX.Element => { const extraData = view.extraData !== '' ? JSON.parse(view.extraData) : ''; let bgColor = getRandomColor(); if (extraData !== '') { bgColor = extraData.color; } const formattedDateAndTime = formatTimezoneAdjustedTimestamp( view.createdAt, DATE_TIME_FORMATS.DASH_TIME_DATE, ); const isEditDeleteSupported = allowedRoles.includes(user.role as string); return (
{' '} {view.name}
handleEditModelOpen(view, bgColor)} /> handleRedirectQuery(view)} data-testid="go-to-explorer" /> handleDeleteModelOpen(view.id, view.name)} />
{view.createdBy.substring(0, 1).toUpperCase()}
{view.createdBy}
{formattedDateAndTime}
); }, }, ]; const paginationConfig = { pageSize: 5, hideOnSinglePage: true }; return (
Views Manage your saved views for {ROUTES_VS_SOURCEPAGE[pathname]}.{' '} Learn more } value={searchValue} onChange={handleSearch} /> Delete view} open={isDeleteModalOpen} closable={false} onCancel={hideDeleteViewModal} footer={[ , , ]} > {t('delete_confirm_message', { viewName: activeViewName, })} Edit view details} open={isEditModalOpen} closable={false} onCancel={hideEditViewModal} footer={[ , ]} > Label
setColor(hex)} /> setNewViewName(e.target.value)} />
); } export default SaveView;