From f61dc87a9a857ca474e5b925a51fc66fc6124be2 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Fri, 27 Sep 2024 16:53:50 +0530 Subject: [PATCH 1/3] support for dashbaord templtes --- src/@types/parseable/api/dashboards.ts | 4 + src/api/dashboard.ts | 3 +- src/hooks/useDashboards.tsx | 26 +++ src/pages/Dashboards/SideBar.tsx | 64 ++++++-- src/pages/Dashboards/Toolbar.tsx | 2 +- src/pages/Dashboards/assets/templates.tsx | 192 ++++++++++++++++++++++ 6 files changed, 276 insertions(+), 15 deletions(-) create mode 100644 src/pages/Dashboards/assets/templates.tsx 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/SideBar.tsx b/src/pages/Dashboards/SideBar.tsx index e2006edb..cc02f151 100644 --- a/src/pages/Dashboards/SideBar.tsx +++ b/src/pages/Dashboards/SideBar.tsx @@ -1,13 +1,15 @@ import { DASHBOARDS_SIDEBAR_WIDTH } from '@/constants/theme'; -import { Box, Button, FileInput, Modal, px, ScrollArea, Stack, Text } from '@mantine/core'; +import { Box, Button, Divider, FileInput, Modal, 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 _ from 'lodash'; -import { Dashboard } from '@/@types/parseable/api/dashboards'; +import { Dashboard, ImportDashboardType } from '@/@types/parseable/api/dashboards'; import IconButton from '@/components/Button/IconButton'; import { useDashboardsQuery } from '@/hooks/useDashboards'; +import { templates } from './assets/templates'; +import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; const { selectDashboard, toggleCreateDashboardModal, toggleImportDashboardModal } = dashboardsStoreReducers; @@ -67,14 +69,50 @@ const DashboardList = (props: { updateTimeRange: (dashboard: Dashboard) => void ); }; +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 { createDashboard, isCreatingDashboard } = useDashboardsQuery({}); + 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; @@ -85,16 +123,10 @@ const ImportDashboardModal = () => { const target = e.target; if (target === null || typeof target.result !== 'string') return; - const newDashboard = JSON.parse(target.result); + const newDashboard: ImportDashboardType = JSON.parse(target.result); if (_.isEmpty(newDashboard)) return; - return createDashboard({ - dashboard: newDashboard, - onSuccess: () => { - closeModal(); - setFile(null); - }, - }); + return makePostCall(newDashboard) } catch (error) {} }; reader.readAsText(file); @@ -115,10 +147,16 @@ const ImportDashboardModal = () => { }} title={Import Dashboard}> + {!isStandAloneMode && ( + <> + + + + )} { - diff --git a/src/pages/Dashboards/Toolbar.tsx b/src/pages/Dashboards/Toolbar.tsx index ad6d6d1b..43336476 100644 --- a/src/pages/Dashboards/Toolbar.tsx +++ b/src/pages/Dashboards/Toolbar.tsx @@ -245,7 +245,7 @@ const ImportTileModal = () => { Date: Mon, 30 Sep 2024 19:21:46 +0530 Subject: [PATCH 2/3] autopaging of tiles --- src/pages/Dashboards/Dashboard.tsx | 49 ++++++++++++++++--- .../providers/DashboardsProvider.ts | 46 +++++++++++++---- 2 files changed, 80 insertions(+), 15 deletions(-) diff --git a/src/pages/Dashboards/Dashboard.tsx b/src/pages/Dashboards/Dashboard.tsx index 3ae1367d..dae4010d 100644 --- a/src/pages/Dashboards/Dashboard.tsx +++ b/src/pages/Dashboards/Dashboard.tsx @@ -6,27 +6,64 @@ 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 } from 'react'; import { makeExportClassName } from '@/utils/exportImage'; import { useDashboardsQuery } from '@/hooks/useDashboards'; import Tile from './Tile'; import { Layout } from 'react-grid-layout'; -const { toggleCreateDashboardModal, toggleCreateTileModal, toggleDeleteTileModal } = dashboardsStoreReducers; +const { toggleCreateDashboardModal, toggleCreateTileModal, toggleDeleteTileModal, handlePaging } = + 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 ( - + { return _.chain(idsByOrder) .map((tile_id, index) => { @@ -19,9 +21,10 @@ export const assignOrderToTiles = (tiles: Tile[]) => { }); }; -export const genLayout = (tiles: Tile[]): Layout[] => { +export const genLayout = (tiles: Tile[], currentPage: number): Layout[] => { + const tilesToShow = _.take(tiles, TILES_PER_PAGE * currentPage); return _.reduce( - tiles, + tilesToShow, (acc: Layout[], tile) => { const { visualization: { size }, @@ -85,6 +88,7 @@ type DashboardsStore = { deleteTileId: string | null; importTileModalOpen: boolean; importDashboardModalOpen: boolean; + currentPage: number; }; const initialState: DashboardsStore = { @@ -102,7 +106,8 @@ const initialState: DashboardsStore = { deleteTileModalOpen: false, deleteTileId: null, importTileModalOpen: false, - importDashboardModalOpen: false + importDashboardModalOpen: false, + currentPage: 1, }; type ReducerOutput = Partial; @@ -121,6 +126,7 @@ type DashboardsStoreReducers = { resetTilesData: (store: DashboardsStore) => ReducerOutput; toggleImportTileModal: (store: DashboardsStore, val: boolean) => ReducerOutput; toggleImportDashboardModal: (store: DashboardsStore, val: boolean) => ReducerOutput; + handlePaging: (store: DashboardsStore) => ReducerOutput; }; const toggleCreateDashboardModal = (_store: DashboardsStore, val: boolean) => { @@ -173,7 +179,7 @@ const toggleAllowDrag = (store: DashboardsStore) => { }; const setDashboards = (store: DashboardsStore, dashboards: Dashboard[]) => { - const { activeDashboard: activeDashboardFromStore } = store; + const { activeDashboard: activeDashboardFromStore, currentPage } = store; const defaultActiveDashboard = _.isArray(dashboards) && !_.isEmpty(dashboards) ? dashboards[0] : null; const activeDashboard = (() => { if (activeDashboardFromStore) { @@ -185,10 +191,23 @@ const setDashboards = (store: DashboardsStore, dashboards: Dashboard[]) => { } })(); + const activeDashboardTilesCount = _.size(activeDashboard?.tiles); + const updatedCurrentPage = (() => { + if (!activeDashboard) { + return 1; + } else if (activeDashboardTilesCount === TILES_PER_PAGE * currentPage + 1) { + // to handle import tile + return currentPage + 1; + } else { + return currentPage; + } + })(); + return { dashboards, activeDashboard, - layout: activeDashboard ? genLayout(activeDashboard.tiles) : [], + layout: activeDashboard ? genLayout(activeDashboard.tiles, updatedCurrentPage) : [], + currentPage: updatedCurrentPage, }; }; @@ -203,7 +222,7 @@ const selectDashboard = (store: DashboardsStore, dashboardId?: string | null, da ...initialState, dashboards: store.dashboards, activeDashboard: activeDashboard || null, - layout: activeDashboard ? genLayout(activeDashboard.tiles) : [], + layout: activeDashboard ? genLayout(activeDashboard.tiles, initialState.currentPage) : [], }; }; @@ -223,14 +242,22 @@ const toggleDeleteTileModal = (_store: DashboardsStore, val: boolean, tileId: st }; }; - - const resetTilesData = (_store: DashboardsStore) => { return { tilesData: {}, }; }; +const handlePaging = (store: DashboardsStore) => { + const { currentPage, activeDashboard } = store; + const newCurrentPage = currentPage + 1; + const layout = genLayout(activeDashboard?.tiles || [], newCurrentPage); + return { + currentPage: newCurrentPage, + layout, + }; +}; + const { Provider: DashbaordsProvider, useStore: useDashboardsStore } = initContext(initialState); const dashboardsStoreReducers: DashboardsStoreReducers = { @@ -246,7 +273,8 @@ const dashboardsStoreReducers: DashboardsStoreReducers = { toggleDeleteTileModal, resetTilesData, toggleImportTileModal, - toggleImportDashboardModal + toggleImportDashboardModal, + handlePaging, }; export { DashbaordsProvider, useDashboardsStore, dashboardsStoreReducers }; From fa5e32089fba2600107fd8c755acd3b27f543234 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Wed, 2 Oct 2024 17:11:46 +0530 Subject: [PATCH 3/3] add import template btn to empty dashboards view --- src/pages/Dashboards/Dashboard.tsx | 137 +++++++++++++++++++++++++++-- src/pages/Dashboards/SideBar.tsx | 122 +------------------------ 2 files changed, 135 insertions(+), 124 deletions(-) diff --git a/src/pages/Dashboards/Dashboard.tsx b/src/pages/Dashboards/Dashboard.tsx index dae4010d..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'; @@ -14,13 +14,16 @@ import { } from './providers/DashboardsProvider'; import _ from 'lodash'; import { IconLayoutDashboard } from '@tabler/icons-react'; -import { useCallback, useEffect, 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, handlePaging } = +const { toggleCreateDashboardModal, toggleCreateTileModal, toggleDeleteTileModal, handlePaging, toggleImportDashboardModal } = dashboardsStoreReducers; const TilesView = (props: { onLayoutChange: (layout: Layout[]) => void }) => { @@ -148,6 +151,115 @@ const DeleteTileModal = () => { ); }; +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); @@ -155,8 +267,13 @@ const NoDashboardsView = () => { setDashboardsStore((store) => toggleCreateDashboardModal(store, true)); }, []); + const openImportDashboardModal = useCallback(() => { + setDashboardsStore((store) => toggleImportDashboardModal(store, true)); + }, []); + return ( + @@ -164,9 +281,16 @@ const NoDashboardsView = () => { Create your first dashboard to visualize log events from various streams. - - - + + + + + + + + ); }; @@ -210,6 +334,7 @@ const Dashboard = () => { + ); diff --git a/src/pages/Dashboards/SideBar.tsx b/src/pages/Dashboards/SideBar.tsx index cc02f151..f2eb192f 100644 --- a/src/pages/Dashboards/SideBar.tsx +++ b/src/pages/Dashboards/SideBar.tsx @@ -1,18 +1,14 @@ import { DASHBOARDS_SIDEBAR_WIDTH } from '@/constants/theme'; -import { Box, Button, Divider, 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, ImportDashboardType } from '@/@types/parseable/api/dashboards'; +import { Dashboard } from '@/@types/parseable/api/dashboards'; import IconButton from '@/components/Button/IconButton'; -import { useDashboardsQuery } from '@/hooks/useDashboards'; -import { templates } from './assets/templates'; -import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; -const { selectDashboard, toggleCreateDashboardModal, toggleImportDashboardModal } = - dashboardsStoreReducers; +const { selectDashboard, toggleCreateDashboardModal, toggleImportDashboardModal } = dashboardsStoreReducers; interface DashboardItemProps extends Dashboard { activeDashboardId: undefined | string; onSelect: (id: string) => void; @@ -69,115 +65,6 @@ const DashboardList = (props: { updateTimeRange: (dashboard: Dashboard) => void ); }; -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 renderShareIcon = () => ; const ImportDashboardButton = () => { @@ -199,7 +86,6 @@ const SideBar = (props: { updateTimeRange: (dashboard: Dashboard) => void }) => return ( -