diff --git a/src/pages/Dashboards/CreateTileForm.tsx b/src/pages/Dashboards/CreateTileForm.tsx index 4609ee39..95639f83 100644 --- a/src/pages/Dashboards/CreateTileForm.tsx +++ b/src/pages/Dashboards/CreateTileForm.tsx @@ -150,6 +150,7 @@ const DataPreview = (props: { form: TileFormType }) => { } = props; const containerRef: MutableRefObject = useRef(null); const [containerSize, setContainerSize] = useState({ height: 0, width: 0 }); + const errorMsg = getErrorMsg(props.form, 'data'); useEffect(() => { if (containerRef.current) { setContainerSize({ @@ -157,18 +158,17 @@ const DataPreview = (props: { form: TileFormType }) => { width: containerRef.current?.offsetWidth || 0, }); } - }, []); - const errorMsg = getErrorMsg(props.form, 'data'); + }, [errorMsg]); return ( - - - - {errorMsg ? ( - - ) : ( + {errorMsg ? ( + + ) : ( + + + { styles={{ copy: { marginLeft: '550px' } }} copyLabel="Copy Records" /> - )} + - - + + )} ); }; diff --git a/src/pages/Dashboards/SideBar.tsx b/src/pages/Dashboards/SideBar.tsx index 4e0d542b..e2006edb 100644 --- a/src/pages/Dashboards/SideBar.tsx +++ b/src/pages/Dashboards/SideBar.tsx @@ -1,13 +1,16 @@ import { DASHBOARDS_SIDEBAR_WIDTH } from '@/constants/theme'; -import { Button, ScrollArea, Stack, Text } from '@mantine/core'; +import { Box, Button, FileInput, Modal, px, ScrollArea, Stack, Text } from '@mantine/core'; import classes from './styles/sidebar.module.css'; -import { IconPlus } from '@tabler/icons-react'; +import { IconFileDownload, IconPlus } from '@tabler/icons-react'; import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; -import { useCallback } from 'react'; +import { useCallback, useState } 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 } = dashboardsStoreReducers; +const { selectDashboard, toggleCreateDashboardModal, toggleImportDashboardModal } = + dashboardsStoreReducers; interface DashboardItemProps extends Dashboard { activeDashboardId: undefined | string; onSelect: (id: string) => void; @@ -64,6 +67,89 @@ 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 = () => { + const [_store, setDashbaordsStore] = useDashboardsStore((_store) => null); + const onClick = useCallback(() => { + setDashbaordsStore((store) => toggleImportDashboardModal(store, true)); + }, []); + return ; +}; + const SideBar = (props: { updateTimeRange: (dashboard: Dashboard) => void }) => { const [dashboards, setDashbaordsStore] = useDashboardsStore((store) => store.dashboards); @@ -75,7 +161,8 @@ const SideBar = (props: { updateTimeRange: (dashboard: Dashboard) => void }) => return ( - + + + diff --git a/src/pages/Dashboards/Tile.tsx b/src/pages/Dashboards/Tile.tsx index a8f64a91..0089ced3 100644 --- a/src/pages/Dashboards/Tile.tsx +++ b/src/pages/Dashboards/Tile.tsx @@ -7,6 +7,7 @@ import { IconGripVertical, IconPencil, IconPhoto, + IconShare, IconTable, IconTrash, } from '@tabler/icons-react'; @@ -25,10 +26,51 @@ import { useCallback, useEffect } from 'react'; import { Tile as TileType, TileQueryResponse } from '@/@types/parseable/api/dashboards'; import { sanitiseSqlString } from '@/utils/sanitiseSqlString'; import Table from './Table'; -import { downloadDataAsCSV, downloadDataAsJson } from '@/utils/exportHelpers'; +import { downloadDataAsCSV, downloadDataAsJson, exportJson } from '@/utils/exportHelpers'; import { makeExportData, useLogsStore } from '../Stream/providers/LogsProvider'; import { getRandomUnitTypeForChart, getUnitTypeByKey } from './utils'; +const ParseableLogo = () => ( +
+ + + + + + + + + + + + +
+); + const { toggleCreateTileModal, toggleDeleteTileModal } = dashboardsStoreReducers; const NoDataView = () => { @@ -118,7 +160,7 @@ function TileControls(props: { tile: TileType; data: TileQueryResponse }) { downloadDataAsCSV(makeExportData(records, fields, 'CSV'), name); }, [props.data]); - const exportJson = useCallback(() => { + const exportDataAsJson = useCallback(() => { downloadDataAsJson(makeExportData(records, fields, 'JSON'), name); }, [props.data]); @@ -130,50 +172,63 @@ function TileControls(props: { tile: TileType; data: TileQueryResponse }) { setDashboardsStore((store) => toggleDeleteTileModal(store, true, tile_id)); }, []); + const exportTileConfig = useCallback(async () => { + const santizedConfig = _.omit(props.tile, 'tile_id'); + return exportJson(JSON.stringify(santizedConfig, null, 2), name) + }, [name]); + if (allowDrag) return ; return ( - - - - - - Actions - }> - Edit - - }> - Delete - - - Exports - }> - PNG - - }> - CSV - - }> - JSON - - - +
+ + + + + + Actions + }> + Edit + + }> + Share + + }> + Delete + + + Exports + }> + PNG + + }> + CSV + + }> + JSON + + + +
); } @@ -205,22 +260,27 @@ const Tile = (props: { id: string }) => { const Viz = getViz(vizType); return ( - - + + - + {tile.name} - + {tile.description} + {timeRange.label} + {isLoading && } {!hasData && !isLoading && } {!isLoading && hasData && ( - + {Viz && } )} diff --git a/src/pages/Dashboards/Toolbar.tsx b/src/pages/Dashboards/Toolbar.tsx index a2457497..ad6d6d1b 100644 --- a/src/pages/Dashboards/Toolbar.tsx +++ b/src/pages/Dashboards/Toolbar.tsx @@ -1,6 +1,6 @@ import TimeRange from '@/components/Header/TimeRange'; -import { Box, Button, Modal, px, Stack, Text, TextInput } from '@mantine/core'; -import { IconCheck, IconPencil, IconPlus, IconTrash } from '@tabler/icons-react'; +import { Box, Button, FileInput, Modal, px, Stack, Text, TextInput } from '@mantine/core'; +import { IconCheck, IconFileDownload, IconPencil, IconPlus, IconShare, IconTrash } from '@tabler/icons-react'; import classes from './styles/toolbar.module.css'; import { useDashboardsStore, dashboardsStoreReducers, sortTilesByOrder } from './providers/DashboardsProvider'; import { ChangeEvent, useCallback, useState } from 'react'; @@ -8,9 +8,16 @@ import IconButton from '@/components/Button/IconButton'; import { useDashboardsQuery } from '@/hooks/useDashboards'; import _ from 'lodash'; import ReactGridLayout, { Layout } from 'react-grid-layout'; +import { Dashboard } from '@/@types/parseable/api/dashboards'; +import { exportJson } from '@/utils/exportHelpers'; -const { toggleEditDashboardModal, toggleAllowDrag, toggleCreateTileModal, toggleDeleteDashboardModal } = - dashboardsStoreReducers; +const { + toggleEditDashboardModal, + toggleAllowDrag, + toggleCreateTileModal, + toggleDeleteDashboardModal, + toggleImportTileModal, +} = dashboardsStoreReducers; const tileIdsbyOrder = (layout: Layout[]) => { return layout @@ -77,6 +84,26 @@ const AddTileButton = () => { ); }; +const ImportTileButton = () => { + const [, setDashbaordsStore] = useDashboardsStore((_store) => null); + + const onClick = useCallback(() => { + setDashbaordsStore((store) => toggleImportTileModal(store, true)); + }, []); + + return ( + + + + ); +}; + const DeleteDashboardModal = () => { const [activeDashboard, setDashbaordsStore] = useDashboardsStore((store) => store.activeDashboard); const [deleteDashboardModalOpen] = useDashboardsStore((store) => store.deleteDashboardModalOpen); @@ -128,7 +155,7 @@ const DeleteDashboardModal = () => { - + + + + + + + + + ); +}; + const Toolbar = (props: { layoutRef: React.MutableRefObject }) => { const [activeDashboard, setDashbaordsStore] = useDashboardsStore((store) => store.activeDashboard); const openEditDashboardModal = useCallback(() => { @@ -168,6 +283,7 @@ const Toolbar = (props: { layoutRef: React.MutableRefObject + @@ -184,7 +300,9 @@ const Toolbar = (props: { layoutRef: React.MutableRefObject + + diff --git a/src/pages/Dashboards/providers/DashboardsProvider.ts b/src/pages/Dashboards/providers/DashboardsProvider.ts index 5877b2c0..4b7e28c9 100644 --- a/src/pages/Dashboards/providers/DashboardsProvider.ts +++ b/src/pages/Dashboards/providers/DashboardsProvider.ts @@ -83,6 +83,8 @@ type DashboardsStore = { layout: Layout[]; deleteTileModalOpen: boolean; deleteTileId: string | null; + importTileModalOpen: boolean; + importDashboardModalOpen: boolean; }; const initialState: DashboardsStore = { @@ -99,6 +101,8 @@ const initialState: DashboardsStore = { layout: [], deleteTileModalOpen: false, deleteTileId: null, + importTileModalOpen: false, + importDashboardModalOpen: false }; type ReducerOutput = Partial; @@ -115,6 +119,8 @@ type DashboardsStoreReducers = { setTileData: (store: DashboardsStore, tileId: string, data: TileQueryResponse) => ReducerOutput; toggleDeleteTileModal: (store: DashboardsStore, val: boolean, tileId: string | null) => ReducerOutput; resetTilesData: (store: DashboardsStore) => ReducerOutput; + toggleImportTileModal: (store: DashboardsStore, val: boolean) => ReducerOutput; + toggleImportDashboardModal: (store: DashboardsStore, val: boolean) => ReducerOutput; }; const toggleCreateDashboardModal = (_store: DashboardsStore, val: boolean) => { @@ -148,6 +154,18 @@ const toggleDeleteDashboardModal = (_store: DashboardsStore, val: boolean) => { }; }; +const toggleImportTileModal = (_store: DashboardsStore, val: boolean) => { + return { + importTileModalOpen: val, + }; +}; + +const toggleImportDashboardModal = (_store: DashboardsStore, val: boolean) => { + return { + importDashboardModalOpen: val, + }; +}; + const toggleAllowDrag = (store: DashboardsStore) => { return { allowDrag: !store.allowDrag, @@ -205,6 +223,8 @@ const toggleDeleteTileModal = (_store: DashboardsStore, val: boolean, tileId: st }; }; + + const resetTilesData = (_store: DashboardsStore) => { return { tilesData: {}, @@ -225,6 +245,8 @@ const dashboardsStoreReducers: DashboardsStoreReducers = { setTileData, toggleDeleteTileModal, resetTilesData, + toggleImportTileModal, + toggleImportDashboardModal }; export { DashbaordsProvider, useDashboardsStore, dashboardsStoreReducers }; diff --git a/src/pages/Dashboards/styles/tile.module.css b/src/pages/Dashboards/styles/tile.module.css index 76500424..86812ad8 100644 --- a/src/pages/Dashboards/styles/tile.module.css +++ b/src/pages/Dashboards/styles/tile.module.css @@ -23,7 +23,7 @@ .tileDescription { color: var(--mantine-color-gray-6); - font-size: 0.65rem; + font-size: 0.75rem; } .tileContainer { @@ -65,4 +65,11 @@ .dragIcon { color: var(--mantine-color-brandPrimary-3); -} \ No newline at end of file +} + +.tileTimeRangeText { + font-size: 0.65rem; + font-weight: 400; + color: var(--mantine-color-gray-6); + display: none; +} diff --git a/src/utils/exportHelpers.ts b/src/utils/exportHelpers.ts index a11748fe..6ca38a71 100644 --- a/src/utils/exportHelpers.ts +++ b/src/utils/exportHelpers.ts @@ -3,10 +3,7 @@ import _ from 'lodash'; type Data = Log[] | null; -export const downloadDataAsJson = (data: Data, filename: string) => { - if (data === null || data.length === 0) return; - - const jsonString = JSON.stringify(data, null, 2); +export const exportJson = (jsonString: string, filename: string) => { const blob = new Blob([jsonString], { type: 'application/json' }); const downloadLink = document.createElement('a'); downloadLink.href = URL.createObjectURL(blob); @@ -16,6 +13,13 @@ export const downloadDataAsJson = (data: Data, filename: string) => { document.body.removeChild(downloadLink); }; +export const downloadDataAsJson = (data: Data, filename: string) => { + if (data === null || data.length === 0) return; + + const jsonString = JSON.stringify(data, null, 2); + return exportJson(jsonString, filename); +}; + export const downloadDataAsCSV = (data: Data, filename: string) => { if (data === null || data.length === 0) return; diff --git a/src/utils/exportImage.ts b/src/utils/exportImage.ts index e3d512ff..a9a908ae 100644 --- a/src/utils/exportImage.ts +++ b/src/utils/exportImage.ts @@ -6,12 +6,62 @@ export const makeExportClassName = (name: string) => { return `png-capture-${sanitizedName}`; }; +const onCloneCallback = (element: HTMLElement) => { + const containerDiv = element.querySelector('.png-export-tile-container') as HTMLElement; + const menuIcon = element.querySelector('.png-export-menu-icon') as HTMLElement; + const tileTitleDiv = element.querySelector('.png-export-tile-title') as HTMLElement; + const headerDiv = element.querySelector('.png-export-tile-header') as HTMLElement; + const tileDescriptionDiv = element.querySelector('.png-export-tile-description') as HTMLElement; + const timeRangeText = element.querySelector('.png-export-tile-timerange') as HTMLElement; + const logoImage = element.querySelector('.png-export-parseable-logo') as HTMLElement; + if (headerDiv) { + headerDiv.style.height = 'auto'; + headerDiv.style.alignItems = 'flex-start'; + } + + if (containerDiv) { + containerDiv.style.border = 'none'; + } + + if (menuIcon) { + menuIcon.remove(); + } + + if (tileTitleDiv) { + tileTitleDiv.style.setProperty('--text-line-clamp', 'none'); + } + + if (tileDescriptionDiv) { + tileDescriptionDiv.style.setProperty('--text-line-clamp', 'none'); + } + + if (element) { + element.style.height = 'auto'; + } + + if (timeRangeText) { + timeRangeText.style.display = 'block'; + } + + if (logoImage) { + logoImage.style.display = 'block'; + logoImage.style.marginTop = '4px'; + logoImage.style.marginLeft = '4px'; + } +}; + const handleCapture = (opts: { className: string; fileName: string }) => { const { className, fileName = 'png-export' } = opts; try { const element = document.querySelector(`.${className}`) as HTMLElement; if (element) { - html2canvas(element).then((canvas) => { + html2canvas(element, { + scale: 3, + backgroundColor: null, + onclone(_document, element) { + onCloneCallback(element); + }, + }).then((canvas) => { const imgData = canvas.toDataURL('image/png'); const link = document.createElement('a'); link.href = imgData; diff --git a/src/utils/index.ts b/src/utils/index.ts index 5862a270..2f7b11b3 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -97,3 +97,11 @@ export const addOrRemoveElement = (array: any[], element: any) => { return dup; } }; + +export const copyTextToClipboard = async (value: any) => { + if (_.isString(value)) { + return await navigator.clipboard.writeText(value); + } else { + return await navigator.clipboard.writeText(JSON.stringify(value, null, 2)); + } +}; \ No newline at end of file