/* eslint-disable no-nested-ternary */ /* eslint-disable jsx-a11y/img-redundant-alt */ /* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/no-static-element-interactions */ import './DashboardList.styles.scss'; import { Color } from '@signozhq/design-tokens'; import { Button, Dropdown, Flex, Input, MenuProps, Modal, Popover, Skeleton, Switch, Table, Tag, Tooltip, Typography, } from 'antd'; import { TableProps } from 'antd/lib'; import logEvent from 'api/common/logEvent'; import createDashboard from 'api/dashboard/create'; import { AxiosError } from 'axios'; import cx from 'classnames'; import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; import { dashboardListMessage } from 'components/facingIssueBtn/util'; import { ENTITY_VERSION_V4 } from 'constants/app'; import ROUTES from 'constants/routes'; import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils'; import dayjs from 'dayjs'; import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; import useComponentPermission from 'hooks/useComponentPermission'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; import { get, isEmpty, isUndefined } from 'lodash-es'; import { ArrowDownWideNarrow, ArrowUpRight, CalendarClock, Check, Clock4, Ellipsis, EllipsisVertical, Expand, HdmiPort, LayoutGrid, Link2, Plus, Radius, RotateCw, Search, } from 'lucide-react'; import { handleContactSupport } from 'pages/Integrations/utils'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { ChangeEvent, Key, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { generatePath } from 'react-router-dom'; import { useCopyToClipboard } from 'react-use'; import { AppState } from 'store/reducers'; import { Dashboard } from 'types/api/dashboard/getAll'; import AppReducer from 'types/reducer/app'; import { isCloudUser } from 'utils/app'; import useUrlQuery from '../../hooks/useUrlQuery'; import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal'; import ImportJSON from './ImportJSON'; import { DeleteButton } from './TableComponents/DeleteButton'; import { DashboardDynamicColumns, DynamicColumns, filterDashboard, } from './utils'; // eslint-disable-next-line sonarjs/cognitive-complexity function DashboardsList(): JSX.Element { const { data: dashboardListResponse = [], isLoading: isDashboardListLoading, error: dashboardFetchError, refetch: refetchDashboardList, } = useGetAllDashboard(); const { role } = useSelector((state) => state.app); const { listSortOrder: sortOrder, setListSortOrder: setSortOrder, } = useDashboard(); const [action, createNewDashboard] = useComponentPermission( ['action', 'create_new_dashboards'], role, ); const [searchValue, setSearchValue] = useState(''); const [ showNewDashboardTemplatesModal, setShowNewDashboardTemplatesModal, ] = useState(false); const { t } = useTranslation('dashboard'); const [ isImportJSONModalVisible, setIsImportJSONModalVisible, ] = useState(false); const [uploadedGrafana, setUploadedGrafana] = useState(false); const [isFilteringDashboards, setIsFilteringDashboards] = useState(false); const [isConfigureMetadataOpen, setIsConfigureMetadata] = useState( false, ); const params = useUrlQuery(); const searchParams = params.get('search'); const [searchString, setSearchString] = useState(searchParams || ''); const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => { const dashboardDynamicColumnsString = localStorage.getItem('dashboard'); let dashboardDynamicColumns: DashboardDynamicColumns = { createdAt: true, createdBy: true, updatedAt: false, updatedBy: false, }; if (typeof dashboardDynamicColumnsString === 'string') { try { const tempDashboardDynamicColumns = JSON.parse( dashboardDynamicColumnsString, ); if (isEmpty(tempDashboardDynamicColumns)) { localStorage.setItem('dashboard', JSON.stringify(dashboardDynamicColumns)); } else { dashboardDynamicColumns = { ...tempDashboardDynamicColumns }; } } catch (error) { console.error(error); } } else { localStorage.setItem('dashboard', JSON.stringify(dashboardDynamicColumns)); } return dashboardDynamicColumns; }; const [visibleColumns, setVisibleColumns] = useState( () => getLocalStorageDynamicColumns(), ); function setDynamicColumnsLocalStorage( visibleColumns: DashboardDynamicColumns, ): void { try { localStorage.setItem('dashboard', JSON.stringify(visibleColumns)); } catch (error) { console.error(error); } } const [dashboards, setDashboards] = useState(); const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => { const sortedDashboards = dashboards.sort( (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), ); setDashboards(sortedDashboards); }; const sortDashboardsByUpdatedAt = (dashboards: Dashboard[]): void => { const sortedDashboards = dashboards.sort( (a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(), ); setDashboards(sortedDashboards); }; useEffect(() => { params.set('columnKey', sortOrder.columnKey as string); params.set('order', sortOrder.order as string); params.set('page', sortOrder.pagination || '1'); history.replace({ search: params.toString() }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [sortOrder]); const sortHandle = (key: string): void => { if (!dashboards) return; if (key === 'createdAt') { sortDashboardsByCreatedAt(dashboards); setSortOrder({ columnKey: 'createdAt', order: 'descend', pagination: sortOrder.pagination || '1', }); } else if (key === 'updatedAt') { sortDashboardsByUpdatedAt(dashboards); setSortOrder({ columnKey: 'updatedAt', order: 'descend', pagination: sortOrder.pagination || '1', }); } }; function handlePageSizeUpdate(page: number): void { setSortOrder((order) => ({ ...order, pagination: String(page), })); } useEffect(() => { const filteredDashboards = filterDashboard( searchString, dashboardListResponse, ); if (sortOrder.columnKey === 'updatedAt') { sortDashboardsByUpdatedAt(filteredDashboards || []); } else if (sortOrder.columnKey === 'createdAt') { sortDashboardsByCreatedAt(filteredDashboards || []); } else if (sortOrder.columnKey === 'null') { setSortOrder({ columnKey: 'updatedAt', order: 'descend', pagination: sortOrder.pagination || '1', }); sortDashboardsByUpdatedAt(filteredDashboards || []); } }, [ dashboardListResponse, searchString, setSortOrder, sortOrder.columnKey, sortOrder.pagination, ]); const [newDashboardState, setNewDashboardState] = useState({ loading: false, error: false, errorMessage: '', }); const data: Data[] = dashboards?.map((e) => ({ createdAt: e.created_at, description: e.data.description || '', id: e.uuid, lastUpdatedTime: e.updated_at, name: e.data.title, tags: e.data.tags || [], key: e.uuid, createdBy: e.created_by, isLocked: !!e.isLocked || false, lastUpdatedBy: e.updated_by, image: e.data.image || Base64Icons[0], refetchDashboardList, })) || []; const onNewDashboardHandler = useCallback(async () => { try { logEvent('Dashboard List: Create dashboard clicked', {}); setNewDashboardState({ ...newDashboardState, loading: true, }); const response = await createDashboard({ title: t('new_dashboard_title', { ns: 'dashboard', }), uploadedGrafana: false, version: ENTITY_VERSION_V4, }); if (response.statusCode === 200) { history.push( generatePath(ROUTES.DASHBOARD, { dashboardId: response.payload.uuid, }), ); } else { setNewDashboardState({ ...newDashboardState, loading: false, error: true, errorMessage: response.error || 'Something went wrong', }); } } catch (error) { setNewDashboardState({ ...newDashboardState, error: true, errorMessage: (error as AxiosError).toString() || 'Something went Wrong', }); } }, [newDashboardState, t]); const onModalHandler = (uploadedGrafana: boolean): void => { logEvent('Dashboard List: Import JSON clicked', {}); setIsImportJSONModalVisible((state) => !state); setUploadedGrafana(uploadedGrafana); }; const handleSearch = (event: ChangeEvent): void => { setIsFilteringDashboards(true); setSearchValue(event.target.value); const searchText = (event as React.BaseSyntheticEvent)?.target?.value || ''; const filteredDashboards = filterDashboard(searchText, dashboardListResponse); setDashboards(filteredDashboards); setIsFilteringDashboards(false); setSearchString(searchText); }; const [state, setCopy] = useCopyToClipboard(); const { notifications } = useNotifications(); useEffect(() => { if (state.error) { notifications.error({ message: t('something_went_wrong', { ns: 'common', }), }); } if (state.value) { notifications.success({ message: t('success', { ns: 'common', }), }); } }, [state.error, state.value, t, notifications]); function getFormattedTime(dashboard: Dashboard, option: string): string { const timeOptions: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }; const formattedTime = new Date(get(dashboard, option, '')).toLocaleTimeString( 'en-US', timeOptions, ); const dateOptions: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric', }; const formattedDate = new Date(get(dashboard, option, '')).toLocaleDateString( 'en-US', dateOptions, ); // Combine time and date return `${formattedDate} ⎯ ${formattedTime}`; } const onLastUpdated = (time: string): string => { const currentTime = dayjs(); const lastRefresh = dayjs(time); const secondsDiff = currentTime.diff(lastRefresh, 'seconds'); const minutedDiff = currentTime.diff(lastRefresh, 'minutes'); const hoursDiff = currentTime.diff(lastRefresh, 'hours'); const daysDiff = currentTime.diff(lastRefresh, 'days'); const monthsDiff = currentTime.diff(lastRefresh, 'months'); if (isEmpty(time)) { return `No updates yet!`; } if (monthsDiff > 0) { return `Last Updated ${monthsDiff} months ago`; } if (daysDiff > 0) { return `Last Updated ${daysDiff} days ago`; } if (hoursDiff > 0) { return `Last Updated ${hoursDiff} hrs ago`; } if (minutedDiff > 0) { return `Last Updated ${minutedDiff} mins ago`; } return `Last Updated ${secondsDiff} sec ago`; }; const columns: TableProps['columns'] = [ { title: 'Dashboards', key: 'dashboard', render: (dashboard: Data): JSX.Element => { const timeOptions: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }; const formattedTime = new Date(dashboard.createdAt).toLocaleTimeString( 'en-US', timeOptions, ); const dateOptions: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric', }; const formattedDate = new Date(dashboard.createdAt).toLocaleDateString( 'en-US', dateOptions, ); // Combine time and date const formattedDateAndTime = `${formattedDate} ⎯ ${formattedTime}`; const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`; const onClickHandler = (event: React.MouseEvent): void => { if (event.metaKey || event.ctrlKey) { window.open(getLink(), '_blank'); } else { history.push(getLink()); } logEvent('Dashboard List: Clicked on dashboard', { dashboardId: dashboard.id, dashboardName: dashboard.name, }); }; return (
dashboard-image {dashboard.name}
{dashboard?.tags && dashboard.tags.length > 0 && (
{dashboard.tags.map((tag) => ( {tag} ))}
)} {action && (
} placement="bottomRight" arrow={false} rootClassName="dashboard-actions" > { e.stopPropagation(); e.preventDefault(); }} /> )}
{formattedDateAndTime}
{dashboard.createdBy && (
{dashboard.createdBy?.substring(0, 1).toUpperCase()}
{dashboard.createdBy}
)} {visibleColumns.updatedAt && (
{onLastUpdated(dashboard.lastUpdatedTime)}
)} {dashboard.lastUpdatedBy && visibleColumns.updatedBy && (
Last Updated By -  
{dashboard.lastUpdatedBy?.substring(0, 1).toUpperCase()}
{dashboard.lastUpdatedBy}
)}
); }, }, ]; const getCreateDashboardItems = useMemo(() => { const menuItems: MenuProps['items'] = [ { label: (
onModalHandler(false)} > Import JSON
), key: '1', }, ]; if (createNewDashboard) { menuItems.unshift({ label: (
{ onNewDashboardHandler(); }} > Create dashboard
), key: '0', }); } return menuItems; }, [createNewDashboard, onNewDashboardHandler]); const showPaginationItem = (total: number, range: number[]): JSX.Element => ( <> {range[0]} — {range[1]} of {total} ); const paginationConfig = data.length > 20 && { pageSize: 20, showTotal: showPaginationItem, showSizeChanger: false, onChange: (page: any): void => handlePageSizeUpdate(page), current: Number(sortOrder.pagination), defaultCurrent: Number(sortOrder.pagination) || 1, hideOnSinglePage: true, }; const logEventCalledRef = useRef(false); useEffect(() => { if ( !logEventCalledRef.current && !isDashboardListLoading && !isUndefined(dashboardListResponse) ) { logEvent('Dashboard List: Page visited', { number: dashboardListResponse?.length, }); logEventCalledRef.current = true; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isDashboardListLoading]); return (
Dashboards Create and manage dashboards for your workspace.
{isDashboardListLoading || isFilteringDashboards ? (
) : dashboardFetchError ? (
something went wrong Something went wrong :/ Please retry or contact support.
) : dashboards?.length === 0 && !searchValue ? (
dashboards
No dashboards yet.{' '} Create a dashboard to start visualizing your data
{createNewDashboard && (
)}
) : ( <>
} value={searchValue} onChange={handleSearch} /> {createNewDashboard && ( )}
{dashboards?.length === 0 ? (
img No dashboards found for {searchValue}. Create a new dashboard?
) : ( <>
All Dashboards
Sort By
} rootClassName="sort-dashboards" placement="bottomRight" arrow={false} >
} rootClassName="configure-group" placement="bottomRight" arrow={false} >
)} )} onModalHandler(false)} /> { setShowNewDashboardTemplatesModal(false); }} /> { setIsConfigureMetadata(false); // reset to default if the changes are not applied setVisibleColumns(getLocalStorageDynamicColumns()); }} title="Configure Metadata" footer={ } rootClassName="configure-metadata-root" >
dashboard-image {dashboards?.[0]?.data?.title}
{visibleColumns.createdAt && ( {getFormattedTime(dashboards?.[0] as Dashboard, 'created_at')} )} {visibleColumns.createdBy && (
{dashboards?.[0]?.created_by?.substring(0, 1).toUpperCase()} {dashboards?.[0]?.created_by}
)}
{visibleColumns.updatedAt && ( {onLastUpdated(dashboards?.[0]?.updated_at || '')} )} {visibleColumns.updatedBy && (
{dashboards?.[0]?.updated_by?.substring(0, 1).toUpperCase()} {dashboards?.[0]?.updated_by}
)}
Created at
setVisibleColumns((prev) => ({ ...prev, [DynamicColumns.CREATED_AT]: check, })) } />
Created by
setVisibleColumns((prev) => ({ ...prev, [DynamicColumns.CREATED_BY]: check, })) } />
Updated at
setVisibleColumns((prev) => ({ ...prev, [DynamicColumns.UPDATED_AT]: check, })) } />
Updated by
setVisibleColumns((prev) => ({ ...prev, [DynamicColumns.UPDATED_BY]: check, })) } />
); } export interface Data { key: Key; name: string; description: string; tags: string[]; createdBy: string; createdAt: string; lastUpdatedTime: string; lastUpdatedBy: string; isLocked: boolean; id: string; image?: string; } export default DashboardsList;