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 { getRandomColor } from 'container/ExplorerOptions/utils'; 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 { ChangeEvent, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { AppState } from 'store/reducers'; import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery'; import { ViewProps } from 'types/api/saveViews/types'; import { DataSource } from 'types/common/queryBuilder'; import AppReducer from 'types/reducer/app'; 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 { role } = useSelector((state) => state.app); 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.uuid); 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, }); } 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(); }, onError: (err) => { showErrorNotification(notifications, err); }, }, ); }; const { handleExplorerTabChange } = useHandleExplorerTabChange(); const handleRedirectQuery = (view: ViewProps): void => { const currentViewDetails = getViewDetailsUsingViewKey( view.uuid, viewsData?.data.data, ); if (!currentViewDetails) return; const { query, name, uuid, panelType: currentPanelType } = currentViewDetails; if (sourcepage) { handleExplorerTabChange( currentPanelType, { query, name, uuid, }, SOURCEPAGE_VS_ROUTES[sourcepage], ); } }; 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 timeOptions: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }; const formattedTime = new Date(view.createdAt).toLocaleTimeString( 'en-US', timeOptions, ); const dateOptions: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric', }; const formattedDate = new Date(view.createdAt).toLocaleDateString( 'en-US', dateOptions, ); // Combine time and date const formattedDateAndTime = `${formattedTime} ⎯ ${formattedDate}`; const isEditDeleteSupported = allowedRoles.includes(role as string); return (
{' '} {view.name}
handleEditModelOpen(view, bgColor)} /> handleRedirectQuery(view)} /> handleDeleteModelOpen(view.uuid, 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;