diff --git a/src/@types/parseable/api/dashboards.ts b/src/@types/parseable/api/dashboards.ts index 095f1eeb..c71108dc 100644 --- a/src/@types/parseable/api/dashboards.ts +++ b/src/@types/parseable/api/dashboards.ts @@ -52,6 +52,10 @@ export type UpdateDashboardType = Omit & { tiles: EditTileType[]; }; +export type ImportDashboardType = Omit & { + tiles: EditTileType[]; +}; + export type TileQuery = { query: string; startTime: Date; endTime: Date }; export type TileData = Log[]; diff --git a/src/api/dashboard.ts b/src/api/dashboard.ts index 5452ce03..f6f840b2 100644 --- a/src/api/dashboard.ts +++ b/src/api/dashboard.ts @@ -11,6 +11,7 @@ import _ from 'lodash'; import { CreateDashboardType, Dashboard, + ImportDashboardType, TileQuery, TileQueryResponse, UpdateDashboardType, @@ -26,7 +27,7 @@ export const putDashboard = (dashboardId: string, dashboard: UpdateDashboardType return Axios().put(UPDATE_DASHBOARDS_URL(dashboardId), dashboard); }; -export const postDashboard = (dashboard: CreateDashboardType) => { +export const postDashboard = (dashboard: CreateDashboardType | ImportDashboardType) => { return Axios().post(CREATE_DASHBOARDS_URL, { ...dashboard}); }; diff --git a/src/hooks/useDashboards.tsx b/src/hooks/useDashboards.tsx index ab1adf35..d24bda84 100644 --- a/src/hooks/useDashboards.tsx +++ b/src/hooks/useDashboards.tsx @@ -8,6 +8,7 @@ import { useCallback, useState } from 'react'; import { CreateDashboardType, Dashboard, + ImportDashboardType, TileQuery, TileQueryResponse, UpdateDashboardType, @@ -62,6 +63,29 @@ export const useDashboardsQuery = (opts: { updateTimeRange?: (dashboard: Dashboa }, ); + const { mutate: importDashboard, isLoading: isImportingDashboard } = useMutation( + (data: { dashboard: ImportDashboardType; onSuccess?: () => void }) => postDashboard(data.dashboard), + { + onSuccess: (response, variables) => { + const { dashboard_id } = response.data; + if (_.isString(dashboard_id) && !_.isEmpty(dashboard_id)) { + setDashboardsStore((store) => selectDashboard(store, null, response.data)); + } + fetchDashboards(); + variables.onSuccess && variables.onSuccess(); + notifySuccess({ message: 'Created Successfully' }); + }, + onError: (data: AxiosError) => { + if (isAxiosError(data) && data.response) { + const error = data.response?.data as string; + typeof error === 'string' && notifyError({ message: error }); + } else if (data.message && typeof data.message === 'string') { + notifyError({ message: data.message }); + } + }, + }, + ); + const { mutate: updateDashboard, isLoading: isUpdatingDashboard } = useMutation( (data: { dashboard: UpdateDashboardType; onSuccess?: () => void }) => { return putDashboard(data.dashboard.dashboard_id, data.dashboard); @@ -113,6 +137,8 @@ export const useDashboardsQuery = (opts: { updateTimeRange?: (dashboard: Dashboa isCreatingDashboard, updateDashboard, isUpdatingDashboard, + importDashboard, + isImportingDashboard, deleteDashboard, isDeleting, diff --git a/src/pages/Dashboards/Dashboard.tsx b/src/pages/Dashboards/Dashboard.tsx index 3ae1367d..f33b8882 100644 --- a/src/pages/Dashboards/Dashboard.tsx +++ b/src/pages/Dashboards/Dashboard.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Modal, Stack, Text } from '@mantine/core'; +import { Box, Button, Divider, FileInput, Modal, Stack, Text } from '@mantine/core'; import Toolbar from './Toolbar'; import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; @@ -6,27 +6,67 @@ import './styles/ReactGridLayout.css'; import GridLayout from 'react-grid-layout'; import { DASHBOARDS_SIDEBAR_WIDTH, NAVBAR_WIDTH } from '@/constants/theme'; import classes from './styles/DashboardView.module.css'; -import { useDashboardsStore, dashboardsStoreReducers, assignOrderToTiles } from './providers/DashboardsProvider'; +import { + useDashboardsStore, + dashboardsStoreReducers, + assignOrderToTiles, + TILES_PER_PAGE, +} from './providers/DashboardsProvider'; import _ from 'lodash'; import { IconLayoutDashboard } from '@tabler/icons-react'; -import { useCallback, useRef } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { makeExportClassName } from '@/utils/exportImage'; import { useDashboardsQuery } from '@/hooks/useDashboards'; import Tile from './Tile'; import { Layout } from 'react-grid-layout'; +import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; +import { ImportDashboardType } from '@/@types/parseable/api/dashboards'; +import { templates } from './assets/templates'; -const { toggleCreateDashboardModal, toggleCreateTileModal, toggleDeleteTileModal } = dashboardsStoreReducers; +const { toggleCreateDashboardModal, toggleCreateTileModal, toggleDeleteTileModal, handlePaging, toggleImportDashboardModal } = + dashboardsStoreReducers; const TilesView = (props: { onLayoutChange: (layout: Layout[]) => void }) => { - const [activeDashboard] = useDashboardsStore((store) => store.activeDashboard); + const [activeDashboard, setDashbaordsStore] = useDashboardsStore((store) => store.activeDashboard); const [allowDrag] = useDashboardsStore((store) => store.allowDrag); const [layout] = useDashboardsStore((store) => store.layout); - const hasNoTiles = _.size(activeDashboard?.tiles) < 1; + const [currentPage] = useDashboardsStore((store) => store.currentPage); + const scrollRef = useRef(null); + const tilesCount = _.size(activeDashboard?.tiles); + const hasNoTiles = tilesCount < 1; const showNoTilesView = hasNoTiles || !activeDashboard; + const shouldAppendTileRef = useRef(false); + + const handleScroll = useCallback( + _.throttle(() => { + const element = scrollRef.current as HTMLElement | null; + if (element && element.scrollHeight - element.scrollTop === element.clientHeight) { + if (shouldAppendTileRef.current) { + return setDashbaordsStore(handlePaging); + } + } + }, 500), + [], + ); + + useEffect(() => { + const element = scrollRef.current as HTMLElement | null; + if (element) { + element.addEventListener('scroll', handleScroll); + return () => { + element.removeEventListener('scroll', handleScroll); + }; + } + }, []); + + useEffect(() => { + shouldAppendTileRef.current = tilesCount > TILES_PER_PAGE * currentPage; + }, [currentPage, tilesCount]); + if (showNoTilesView) return ; return ( - + { ); }; +const DashboardTemplates = (props: {onImport: (template: ImportDashboardType) => void; isImportingDashboard: boolean}) => { + return ( + + {_.map(templates, (template) => { + return ( + + + {template.name} + + + + + + ); + })} + + ); +} + +const ImportDashboardModal = () => { + const [importDashboardModalOpen, setDashboardStore] = useDashboardsStore((store) => store.importDashboardModalOpen); + const [activeDashboard] = useDashboardsStore((store) => store.activeDashboard); + const [isStandAloneMode] = useAppStore(store => store.isStandAloneMode) + const [file, setFile] = useState(null); + const closeModal = useCallback(() => { + setDashboardStore((store) => toggleImportDashboardModal(store, false)); + }, []); + const { importDashboard, isImportingDashboard } = useDashboardsQuery({}); + const makePostCall = useCallback((dashboard: ImportDashboardType) => { + return importDashboard({ + dashboard, + onSuccess: () => { + closeModal(); + setFile(null); + }, + }); + }, []); + + const onImport = useCallback(() => { + if (activeDashboard === null || file === null) return; + + if (file) { + const reader = new FileReader(); + reader.onload = (e: ProgressEvent) => { + try { + const target = e.target; + if (target === null || typeof target.result !== 'string') return; + + const newDashboard: ImportDashboardType = JSON.parse(target.result); + if (_.isEmpty(newDashboard)) return; + + return makePostCall(newDashboard) + } catch (error) {} + }; + reader.readAsText(file); + } else { + console.error('No file selected.'); + } + }, [activeDashboard, file]); + + return ( + Import Dashboard}> + + {!isStandAloneMode && ( + <> + + + + )} + + + + + + + + + + + + ); +}; + const NoDashboardsView = () => { const [, setDashboardsStore] = useDashboardsStore((_store) => null); @@ -118,8 +267,13 @@ const NoDashboardsView = () => { setDashboardsStore((store) => toggleCreateDashboardModal(store, true)); }, []); + const openImportDashboardModal = useCallback(() => { + setDashboardsStore((store) => toggleImportDashboardModal(store, true)); + }, []); + return ( + @@ -127,9 +281,16 @@ const NoDashboardsView = () => { Create your first dashboard to visualize log events from various streams. - - - + + + + + + + + ); }; @@ -173,6 +334,7 @@ const Dashboard = () => { + ); diff --git a/src/pages/Dashboards/SideBar.tsx b/src/pages/Dashboards/SideBar.tsx index e2006edb..f2eb192f 100644 --- a/src/pages/Dashboards/SideBar.tsx +++ b/src/pages/Dashboards/SideBar.tsx @@ -1,16 +1,14 @@ import { DASHBOARDS_SIDEBAR_WIDTH } from '@/constants/theme'; -import { Box, Button, FileInput, Modal, px, ScrollArea, Stack, Text } from '@mantine/core'; +import { Button, px, ScrollArea, Stack, Text } from '@mantine/core'; import classes from './styles/sidebar.module.css'; import { IconFileDownload, IconPlus } from '@tabler/icons-react'; import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import _ from 'lodash'; import { Dashboard } from '@/@types/parseable/api/dashboards'; import IconButton from '@/components/Button/IconButton'; -import { useDashboardsQuery } from '@/hooks/useDashboards'; -const { selectDashboard, toggleCreateDashboardModal, toggleImportDashboardModal } = - dashboardsStoreReducers; +const { selectDashboard, toggleCreateDashboardModal, toggleImportDashboardModal } = dashboardsStoreReducers; interface DashboardItemProps extends Dashboard { activeDashboardId: undefined | string; onSelect: (id: string) => void; @@ -67,79 +65,6 @@ const DashboardList = (props: { updateTimeRange: (dashboard: Dashboard) => void ); }; -const ImportDashboardModal = () => { - const [importDashboardModalOpen, setDashboardStore] = useDashboardsStore((store) => store.importDashboardModalOpen); - const [activeDashboard] = useDashboardsStore((store) => store.activeDashboard); - const [file, setFile] = useState(null); - const closeModal = useCallback(() => { - setDashboardStore((store) => toggleImportDashboardModal(store, false)); - }, []); - const { createDashboard, isCreatingDashboard } = useDashboardsQuery({}); - const onImport = useCallback(() => { - if (activeDashboard === null || file === null) return; - - if (file) { - const reader = new FileReader(); - reader.onload = (e: ProgressEvent) => { - try { - const target = e.target; - if (target === null || typeof target.result !== 'string') return; - - const newDashboard = JSON.parse(target.result); - if (_.isEmpty(newDashboard)) return; - - return createDashboard({ - dashboard: newDashboard, - onSuccess: () => { - closeModal(); - setFile(null); - }, - }); - } catch (error) {} - }; - reader.readAsText(file); - } else { - console.error('No file selected.'); - } - }, [activeDashboard, file]); - - return ( - Import Dashboard}> - - - - - - - - - - - - - ); -}; - const renderShareIcon = () => ; const ImportDashboardButton = () => { @@ -161,7 +86,6 @@ const SideBar = (props: { updateTimeRange: (dashboard: Dashboard) => void }) => return ( -